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
51 lines
1.7 KiB
Rust
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) {}
|
|
}
|