agent-loop: projection, recovery, compaction into the crate (phase 3)
Nightly Build / build (push) Successful in 6m49s
Nightly Build / build (push) Successful in 6m49s
The session handler is now a thin shell: three entry points in kernel_turn.rs (run_kernel_turn / recover_turn / resolve_pending_call) and the ChatSessionHandler. Everything that shaped a Value — projection, recovery, compaction mechanics, the LLM loop, message building — lives in agent-loop or behind a loop_adapters trait. agent-loop: - projection/ (mod + media): stored history -> wire messages, the one place provider divergence lives; well-formedness contract, DTL injections (append-only), media parts. LinearAssembler is now a Projection + ProjectionHooks config, not its own implementation - recovery.rs: reap interrupted batches -> resolve the deepest frame's non-terminal calls (Running by policy + RestartHint, AwaitingHuman re-asked) -> un-wedge finished children -> cascade up, every frame on its own agent (B3) - compaction.rs: split point (never assistant+tool group), transcript, SUMMARY_PREFIX/preamble/template, the no-tools model call, summary row - manager: resolve_pending (gate skipped, real ToolContext, then continue incl. sub-agent); start_loop used by recovery; LiveInput - delegate: AsyncExecutor + StoreSink for mode:async (durable cron row, result delivered back into the parent conversation) - kernel/context/store: support the above (TurnScope via Extensions, frame lookups, aligned result-text semantics) skald-core: - loop_adapters: UserLoopRuntime (D12 - one LoopManager per user), TurnScope (per-turn state in the Extensions type-map; no scope is denied), projection_cfg/media_source/tool_digest (Skald's projection knobs without owning projection code), async_task (CronExecutor + DurableSink) - session/handler: stripped to mod.rs + kernel_turn.rs + config.rs + interface_tools.rs + media.rs; deleted agent_dispatch, approval, dispatch, emitter, gate, llm_call, llm_loop, message_builder, messages, outcome, resume - compactor.rs: policy only (threshold, model pick, CompactionEvent); mechanics are the crate's CLAUDE.md updated (recovery, compaction, sub-agents, approval gate, projection sections now describe the crate-owned flow).
This commit is contained in:
@@ -218,6 +218,15 @@ pub async fn resolve_tool(
|
||||
let msg = ApprovalDecision::rejection_message(&body.note);
|
||||
if !live {
|
||||
chat_llm_tools::reject(db, tc_id, &msg).await?;
|
||||
// The refusal is part of the conversation: let the model read it and
|
||||
// carry on, instead of leaving the turn dead where it stopped.
|
||||
let hub = ctx.chat_hub.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = hub.resume_session(session_id).await {
|
||||
tracing::warn!(session_id, tool_call_id = tc_id, error = %e,
|
||||
"post-restart continue after rejection failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
return Ok(Json(ResolveToolResponse {
|
||||
tool_call_id: tc_id,
|
||||
@@ -241,48 +250,28 @@ pub async fn resolve_tool(
|
||||
}
|
||||
|
||||
// ── Post-restart path: no in-memory oneshot to unblock. ───────────────────
|
||||
// Sub-agent tools (`execute_task` etc.) cannot run through the flat
|
||||
// `execute_tool` path — they need the recursive dispatcher. Mark the call
|
||||
// pre-approved and drive the owning session's resume, which re-dispatches it
|
||||
// via `execute_tool_call` (gate skipped) and continues the loop. Events stream
|
||||
// to the reconnected client through the global bus; return immediately.
|
||||
if tc_name == "execute_task" || tc_name == tn::EXECUTE_SUBTASK || tc_name == "run_subtask" {
|
||||
let handler = ctx.chat_hub.handler_for_session(session_id).await?;
|
||||
handler.mark_pre_approved(tc_id);
|
||||
let hub = ctx.chat_hub.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = hub.resume_session(session_id).await {
|
||||
tracing::warn!(session_id, tool_call_id = tc_id, error = %e, "post-restart resume of sub-agent tool failed");
|
||||
}
|
||||
});
|
||||
return Ok(Json(ResolveToolResponse {
|
||||
tool_call_id: tc_id,
|
||||
status: "running".to_string(),
|
||||
result: None,
|
||||
result_type: "string".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Simple tools: execute directly on the owning session and return the result.
|
||||
let handler = ctx.chat_hub.handler_for_session(session_id).await?;
|
||||
match handler.execute_tool(&tc_name, args).await {
|
||||
Ok(result) => {
|
||||
let wire = result.to_wire();
|
||||
let kind = result.kind();
|
||||
chat_llm_tools::complete(db, tc_id, &wire, kind).await?;
|
||||
Ok(Json(ResolveToolResponse {
|
||||
tool_call_id: tc_id,
|
||||
status: "done".to_string(),
|
||||
result: Some(wire),
|
||||
result_type: kind.to_string(),
|
||||
}))
|
||||
// One path for every tool: the loop's `resolve_pending` runs the call with
|
||||
// the gate skipped (the human just decided) but with this session's real
|
||||
// context — owner pool, per-user container — then continues the turn. A
|
||||
// sub-agent dispatch works here too: it opens its child frame like any
|
||||
// other call. Events stream to the reconnected client through the global
|
||||
// bus, so the endpoint returns as soon as the work is scheduled.
|
||||
let hub = ctx.chat_hub.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = hub
|
||||
.resolve_pending_call(session_id, tc_id, ApprovalDecision::Approved)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(session_id, tool_call_id = tc_id, error = %e,
|
||||
"post-restart approval failed");
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
chat_llm_tools::fail(db, tc_id, &msg).await?;
|
||||
Err(anyhow::anyhow!(msg).into())
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Json(ResolveToolResponse {
|
||||
tool_call_id: tc_id,
|
||||
status: "running".to_string(),
|
||||
result: None,
|
||||
result_type: "string".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
// ── GET /api/tools/:tool_call_id — full execution detail for the detail page ──
|
||||
|
||||
Reference in New Issue
Block a user