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,42 @@
|
||||
//! Shipped `Model` clients (blueprint D13): OpenAI-compatible, Anthropic,
|
||||
//! Ollama, LM Studio — plus the shared SSE decoder and HTTP helpers.
|
||||
//!
|
||||
//! All clients are stateless (connection config only) and share the same
|
||||
//! failure policy: if a stream dies BEFORE any delta, the client retries
|
||||
//! buffered on the same model (providers rejecting `stream` keep working); a
|
||||
//! mid-stream failure propagates to the caller's fallback logic.
|
||||
|
||||
pub mod anthropic;
|
||||
pub mod lm_studio;
|
||||
pub mod ollama;
|
||||
pub mod openai;
|
||||
mod sse;
|
||||
|
||||
pub use anthropic::AnthropicModel;
|
||||
pub use lm_studio::LmStudioModel;
|
||||
pub use ollama::OllamaModel;
|
||||
pub use openai::OpenAiModel;
|
||||
pub(crate) use sse::SseDecoder;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Converts a reqwest `HeaderMap` into a JSON object (for payload logging).
|
||||
pub(crate) fn headers_to_json(headers: &reqwest::header::HeaderMap) -> Value {
|
||||
let map: serde_json::Map<String, Value> = headers
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("<binary>").into()))
|
||||
.collect();
|
||||
Value::Object(map)
|
||||
}
|
||||
|
||||
/// Raw error body → JSON for the payload log: parsed JSON when the provider
|
||||
/// returned JSON, else the raw text wrapped as a JSON string so a non-JSON
|
||||
/// body (HTML gateway page) is still preserved verbatim.
|
||||
pub(crate) fn error_response_body(text: String) -> Value {
|
||||
serde_json::from_str::<Value>(&text).unwrap_or(Value::String(text))
|
||||
}
|
||||
|
||||
/// Redacted preview of an API key: first 7 chars + "***".
|
||||
pub(crate) fn redact_key(key: &str) -> String {
|
||||
if key.len() > 7 { format!("{}***", &key[..7]) } else { "***".to_string() }
|
||||
}
|
||||
Reference in New Issue
Block a user