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
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
//! `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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user