llm: restore request logging lost in the agent-loop migration
Nightly Build / build (push) Successful in 6m50s
Nightly Build / build (push) Successful in 6m50s
The LLM-requests page has been empty since 24ee5b8: deleting
`session/handler/llm_call.rs` dropped both halves of the request log.
The kernel builds the `ModelRequest` itself and sets `log: None`, so the
`LoggingModel` decorator — wrapped once per model by `LlmManager` — wrote
every metadata row with a NULL `user_id`, while the page (and the detail
endpoint) filter on it. Nothing wrote `llm_request_payloads` at all any
more, so the payload viewer had nothing to show either.
Correlation cannot come from `LlmManager`: it builds one shared client
per model and does not know whose traffic it serves. It now comes from
the `ModelSelector`, the one component that knows both the model and the
owner: `SkaldSelector::with_log(RequestLogTarget)` wraps the model it
hands out, so metadata lands in `llm_requests` attributed to the user and
the payload lands in that user's own encrypted DB, keyed by `request_id`.
Session and frame are read off the request's `conversation`/`frame`,
which makes kernel rounds, sub-agent frames and compaction summaries all
attributed with no extra plumbing (`ModelRequest::log` stays unused).
The compactor's summariser call is attributed too, which it never was:
`try_compact`/`force_compact` now take the owner (its selector is built
per compaction, so it can carry the target).
Three tests in `llm::logging` lock this down — the owner/session/frame
columns, the error row with the provider's rejected body, and the
metadata-only path when no owner pool is available.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ use sqlx::SqlitePool;
|
||||
use crate::approval::ApprovalManager;
|
||||
use crate::clarification::ClarificationManager;
|
||||
use crate::llm::LlmManager;
|
||||
use crate::llm::logging::RequestLogTarget;
|
||||
use crate::loop_adapters::activation::SkaldToolActivator;
|
||||
use crate::loop_adapters::builtins::{SkaldAskUserTool, SkaldHumanChannel};
|
||||
use crate::loop_adapters::history::SqliteHistory;
|
||||
@@ -109,8 +110,12 @@ impl AgentCatalog for SkaldAgentCatalog {
|
||||
let meta = crate::agents::load_task_meta(id).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
// The child's own strength drives its selector (D14) — never the
|
||||
// parent's resolved client.
|
||||
let selector = Arc::new(SkaldSelector::new(self.llm_manager.clone(), meta.strength));
|
||||
// parent's resolved client. Its traffic is logged under the same owner
|
||||
// (the child's frame id already distinguishes it in the log).
|
||||
let selector = Arc::new(
|
||||
SkaldSelector::new(self.llm_manager.clone(), meta.strength)
|
||||
.with_log(RequestLogTarget::user(self.user_id.clone(), self.pool.clone())),
|
||||
);
|
||||
let model = meta.client.as_deref().map(ModelHint::name);
|
||||
|
||||
// The child's system context: its own prompt, no per-turn extras.
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::approval::ApprovalManager;
|
||||
use crate::clarification::ClarificationManager;
|
||||
use crate::config::DatetimeConfig;
|
||||
use crate::llm::LlmManager;
|
||||
use crate::llm::logging::RequestLogTarget;
|
||||
use crate::loop_adapters::activation::{SkaldActivationSource, SkaldToolActivator};
|
||||
use crate::loop_adapters::async_task::CronExecutor;
|
||||
use crate::loop_adapters::builtins::{
|
||||
@@ -132,9 +133,12 @@ impl UserLoopRuntime {
|
||||
}));
|
||||
|
||||
// The default selector has no strength requirement; every turn overrides
|
||||
// it with the agent's own (D14).
|
||||
let default_selector: Arc<dyn ModelSelector> =
|
||||
Arc::new(SkaldSelector::new(llm_manager.clone(), None));
|
||||
// it with the agent's own (D14). It carries the owner's log target, so a
|
||||
// call served by it (recovery, compaction) is still attributed.
|
||||
let default_selector: Arc<dyn ModelSelector> = Arc::new(
|
||||
SkaldSelector::new(llm_manager.clone(), None)
|
||||
.with_log(RequestLogTarget::user(user_id.clone(), pool.clone())),
|
||||
);
|
||||
|
||||
let manager = Arc::new(
|
||||
LoopManager::builder()
|
||||
@@ -208,6 +212,12 @@ impl UserLoopRuntime {
|
||||
&self.store
|
||||
}
|
||||
|
||||
/// Where this user's LLM traffic is logged: metadata in the registry
|
||||
/// (attributed to them), payloads in their own encrypted pool.
|
||||
pub fn log_target(&self) -> RequestLogTarget {
|
||||
RequestLogTarget::user(self.user_id.clone(), self.pool.clone())
|
||||
}
|
||||
|
||||
/// The conversation id of a session — the store's encoding.
|
||||
pub fn conversation(session_id: i64) -> ConversationId {
|
||||
SqliteHistory::conversation(session_id)
|
||||
@@ -259,10 +269,11 @@ impl UserLoopRuntime {
|
||||
extensions.insert(Arc::new(CallerUserId(self.user_id.clone())));
|
||||
extensions.insert(scope.clone());
|
||||
|
||||
// ── Selector: this agent's strength (D14) ──
|
||||
// ── Selector: this agent's strength (D14) + the owner's request log ──
|
||||
let strength = crate::agents::load_meta(&frame_agent).ok().and_then(|m| m.strength);
|
||||
let selector: Arc<dyn ModelSelector> =
|
||||
Arc::new(SkaldSelector::new(self.llm_manager.clone(), strength));
|
||||
let selector: Arc<dyn ModelSelector> = Arc::new(
|
||||
SkaldSelector::new(self.llm_manager.clone(), strength).with_log(self.log_target()),
|
||||
);
|
||||
|
||||
// The session's root frame; the store reuses the provisioned row.
|
||||
let frame = self
|
||||
|
||||
@@ -8,10 +8,11 @@ use std::sync::Arc;
|
||||
|
||||
use agent_loop::activation::ToolRendering;
|
||||
use agent_loop::async_trait;
|
||||
use agent_loop::model::{ModelHandle, ModelHint, ModelInfo, ModelSelector};
|
||||
use agent_loop::model::{Model, ModelHandle, ModelHint, ModelInfo, ModelSelector};
|
||||
use agent_loop::ids::ModelId;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::llm::logging::{LoggingModel, RequestLogTarget};
|
||||
use crate::llm::{DtlMode, LlmEntry, LlmManager, LlmStrength};
|
||||
|
||||
/// Maps Skald's per-model DTL mode to the crate's wire protocol (D15).
|
||||
@@ -37,14 +38,42 @@ pub fn model_info_of(entry: &LlmEntry) -> ModelInfo {
|
||||
|
||||
/// The selector handed to the loop manager for one turn: the manager's
|
||||
/// strength tiering + health + priority, behind the crate's seam.
|
||||
///
|
||||
/// It is also where **request logging** is attached: the selector is the only
|
||||
/// component that knows both the model and the owner of the call, so it wraps
|
||||
/// the model it hands out in a [`LoggingModel`] bound to that owner (see
|
||||
/// [`Self::with_log`]). Without it the metadata row would land with a NULL
|
||||
/// `user_id` and no payload — invisible in the LLM-requests page.
|
||||
pub struct SkaldSelector {
|
||||
manager: Arc<LlmManager>,
|
||||
strength: Option<LlmStrength>,
|
||||
log: Option<RequestLogTarget>,
|
||||
}
|
||||
|
||||
impl SkaldSelector {
|
||||
pub fn new(manager: Arc<LlmManager>, strength: Option<LlmStrength>) -> Self {
|
||||
Self { manager, strength }
|
||||
Self { manager, strength, log: None }
|
||||
}
|
||||
|
||||
/// Attributes every call served by this selector to `target`. Honoured only
|
||||
/// when instance-wide request logging is on (`llm.requests_log.enabled`).
|
||||
pub fn with_log(mut self, target: RequestLogTarget) -> Self {
|
||||
self.log = Some(target);
|
||||
self
|
||||
}
|
||||
|
||||
/// Wraps a resolved client in the logging decorator when both a target and
|
||||
/// the registry pool are available; otherwise hands the bare client over.
|
||||
fn instrument(&self, name: &str, entry: &LlmEntry) -> Arc<dyn Model> {
|
||||
match (&self.log, self.manager.log_pool()) {
|
||||
(Some(target), Some(registry)) => Arc::new(LoggingModel::new(
|
||||
entry.client.clone(),
|
||||
registry,
|
||||
name,
|
||||
target.clone(),
|
||||
)),
|
||||
_ => entry.client.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +89,10 @@ impl ModelSelector for SkaldSelector {
|
||||
let excluded: Vec<&str> = exclude.iter().map(String::as_str).collect();
|
||||
self.manager.select_excluding(&excluded, self.strength).await?
|
||||
};
|
||||
let model = self.instrument(&name, &entry);
|
||||
Ok(ModelHandle {
|
||||
id: name,
|
||||
model: entry.client.clone(),
|
||||
model,
|
||||
info: model_info_of(&entry),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user