Files
Skald-Circle/crates/agent-loop/src/hooks.rs
T
dguiducci b8cc6d263b agent-loop: new crate — LLM loop kernel + Model clients (phase 0)
Extract the LLM agent loop into a standalone workspace crate with zero
deps on skald-core/core-api (blueprint project-loop.md, D13-D15):

- kernel: round loop, model fallback with rebuild, parallel tool fan-out
  (ordered id alloc / bounded concurrent exec / ordered record), streaming
  deltas drained before outcomes, sticky cancellation
- models: OpenAiModel/AnthropicModel/OllamaModel/LmStudioModel ported from
  llm-client onto the Model trait; ModelError carries the HTTP status;
  is_retriable default = the 401/403/404/422 rule
- DTL as crate protocol (ToolRendering Inline/DeferredToolReference/
  SystemToolBlock; Anthropic conversions + Kimi system+tools passthrough),
  host catalog behind ActivationSource/ToolActivator
- HistoryStore durability contract + InMemoryStore; LinearAssembler with
  well-formed projection (incl. DTL injection, summary, crash survivors)
- LoopManager singleton (broadcast bus + live registry), one live loop
  per conversation, orphan-marking on start_turn
- 32 tests green (kernel §13 suite, assembler DTL, SSE/Anthropic ports),
  clippy clean
2026-07-25 23:40:41 +01:00

51 lines
1.7 KiB
Rust

//! `LoopHooks` — the passive/active interception seam. Every host special-case
//! (diff-preview bracketing, per-tool arg normalization, telemetry, discovery)
//! lives here, not in the kernel. All methods default to no-op.
use std::sync::Arc;
use async_trait::async_trait;
use crate::events::{EventSink, PendingToolCall};
use crate::ids::{ConversationId, FrameId, MessageId};
use crate::kernel::TurnOutcome;
use crate::store::{CallOutcome, HistoryStore};
/// Verdict of `pre_tool_call`.
#[derive(Debug, Clone)]
pub enum HookVerdict {
Allow,
Reject { reason: String },
}
/// Context handed to every hook.
pub struct HookCtx {
pub conversation: ConversationId,
pub frame: FrameId,
pub agent: String,
pub store: Arc<dyn HistoryStore>,
pub events: EventSink,
}
#[async_trait]
pub trait LoopHooks: Send + Sync {
async fn before_round(&self, _round: usize, _ctx: &HookCtx) {}
async fn after_round(&self, _round: usize, _ctx: &HookCtx) {}
/// May MUTATE the call's arguments or veto it (Reject). Covers diff-preview
/// bracketing and per-tool normalizations.
async fn pre_tool_call(&self, _call: &mut PendingToolCall, _ctx: &HookCtx) -> HookVerdict {
HookVerdict::Allow
}
/// Covers persistence of activated tools, discovery, file-change
/// notifications, telemetry.
async fn post_tool_call(&self, _call: &PendingToolCall, _outcome: &CallOutcome, _ctx: &HookCtx) {}
async fn on_turn_end(&self, _outcome: &TurnOutcome, _ctx: &HookCtx) {}
/// Fired after a compaction (blueprint §9): hosts re-anchor DTL
/// activations to the first surviving message here.
async fn on_compacted(&self, _frame: FrameId, _covered: MessageId, _first_surviving: MessageId) {}
}