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:
@@ -18,7 +18,9 @@ use crate::cron::TaskManager;
|
||||
use crate::db::{chat_history, chat_llm_tools, chat_sessions, chat_sessions_stack, config, sources};
|
||||
use crate::events::{GlobalEvent, ServerEvent};
|
||||
use crate::notification::Notification;
|
||||
use crate::session::handler::{ChatSessionHandler, InterfaceTool, PendingMsg, PendingUserInput};
|
||||
use crate::session::handler::{
|
||||
ApprovalDecision, ChatSessionHandler, InterfaceTool, PendingMsg, PendingUserInput,
|
||||
};
|
||||
use crate::session::manager::ChatSessionManager;
|
||||
use crate::tools::tool_names as tn;
|
||||
|
||||
@@ -370,7 +372,7 @@ impl ChatHub {
|
||||
}
|
||||
|
||||
/// Resume any interrupted turn for a source's active session.
|
||||
/// Calls `resume_turn` which re-executes pending tool calls (approval or
|
||||
/// Calls `recover_turn`, which re-executes pending tool calls (approval or
|
||||
/// clarification) and re-runs the LLM loop if needed.
|
||||
/// Safe to call unconditionally — returns immediately if there is nothing to resume.
|
||||
/// Events are published to the global broadcast bus so existing subscribers
|
||||
@@ -382,7 +384,7 @@ impl ChatHub {
|
||||
};
|
||||
// Guard against double-driving. A client sends `resume` on connect whenever
|
||||
// history shows a pending/interrupted tool — including when the turn is still
|
||||
// live and merely awaiting an approval. Without this check `resume_turn` would
|
||||
// live and merely awaiting an approval. Without this check the recovery would
|
||||
// block on the `processing` lock and, once the approval unblocks the original
|
||||
// turn and it finishes, run a spurious *second* turn on the just-completed
|
||||
// conversation. If a turn is already in flight it owns the session and emits
|
||||
@@ -408,11 +410,36 @@ impl ChatHub {
|
||||
let tx = Self::bridge_to_global(self.global_tx.clone(), source, session_id);
|
||||
let handler = self.session_mgr.get_or_create_handler(session_id).await?;
|
||||
let interface_tools = self.execute_task_tools(session_id, &handler).await;
|
||||
handler.resume_turn(None, None, interface_tools, tx).await
|
||||
handler.recover_turn(interface_tools, tx).await
|
||||
}
|
||||
|
||||
/// Apply a human decision to a tool call nothing is waiting on anymore (an
|
||||
/// approval answered after a restart), then continue the conversation.
|
||||
/// Events reach the reconnected client through the global bus, as for
|
||||
/// [`Self::resume_session`].
|
||||
pub async fn resolve_pending_call(
|
||||
&self,
|
||||
session_id: i64,
|
||||
call: i64,
|
||||
decision: ApprovalDecision,
|
||||
) -> anyhow::Result<()> {
|
||||
let decision = match decision {
|
||||
ApprovalDecision::Approved => agent_loop::recovery::HumanDecision::Approved,
|
||||
ApprovalDecision::Rejected { note } => agent_loop::recovery::HumanDecision::Rejected {
|
||||
reason: ApprovalDecision::rejection_message(¬e),
|
||||
},
|
||||
};
|
||||
let source = chat_sessions::find_by_id(&self.db, session_id).await?
|
||||
.map(|s| s.source)
|
||||
.unwrap_or_else(|| "web".to_string());
|
||||
let tx = Self::bridge_to_global(self.global_tx.clone(), source, session_id);
|
||||
let handler = self.session_mgr.get_or_create_handler(session_id).await?;
|
||||
let interface_tools = self.execute_task_tools(session_id, &handler).await;
|
||||
handler.resolve_pending_call(call, decision, interface_tools, tx).await
|
||||
}
|
||||
|
||||
/// Builds the `execute_task` interface tool for a session, mirroring the injection
|
||||
/// done for live turns (`run_agent_turn`). Empty when no TaskManager is configured
|
||||
/// done for live turns. Empty when no TaskManager is configured
|
||||
/// so `execute_task mode=async` can be rebuilt by `build_execution` during resume.
|
||||
async fn execute_task_tools(
|
||||
&self,
|
||||
@@ -728,7 +755,7 @@ impl ChatHub {
|
||||
let count = notes.len();
|
||||
// Build a synthetic assistant message with a reasoning trace and a
|
||||
// pre-completed read_notification tool call carrying the notifications as results.
|
||||
// The agent is then woken via resume() — resume_turn sees the tool calls on
|
||||
// The agent is then woken via resume() — recovery sees the tool calls on
|
||||
// the last assistant message and runs the LLM loop so the agent can respond.
|
||||
let result_json = serde_json::to_string(¬es).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user