agent-loop: projection, recovery, compaction into the crate (phase 3)
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:
2026-07-26 17:09:01 +01:00
parent 3fca7867fa
commit 24ee5b89d7
74 changed files with 7661 additions and 5982 deletions
+21 -11
View File
@@ -86,12 +86,7 @@ pub(crate) async fn run(
// ToolCtx extensions: host extensions + the event sink + the turn's tool
// set, so shipped tools (ask_user, activate_tools, delegate) reach what
// they need.
let tool_extensions = || {
let mut ext = params.extensions.clone();
ext.insert(Arc::new(events.clone()));
ext.insert(Arc::new(crate::tool::SharedToolSet(params.tools.clone())));
ext
};
let tool_extensions = || tool_extensions(&params, &events);
events.emit(frame, parent, LoopEvent::TurnStarted);
@@ -289,6 +284,20 @@ pub(crate) async fn run(
finish(TurnOutcome::Exhausted, &deps, &hook_ctx(), &events, frame, parent).await
}
/// What a tool call sees: the host's extensions plus the event sink and the
/// turn's tool set (shipped tools — ask_user, activate_tools, delegate — reach
/// what they need through them). Shared with [`crate::recovery`], which
/// re-executes a call outside a round and must hand it the same context.
pub(crate) fn tool_extensions(
params: &LoopParams,
events: &EventSink,
) -> crate::tool::Extensions {
let mut ext = params.extensions.clone();
ext.insert(Arc::new(events.clone()));
ext.insert(Arc::new(crate::tool::SharedToolSet(params.tools.clone())));
ext
}
/// Terminal helper: hooks.on_turn_end (+ Cancelled event) then return.
async fn finish(
outcome: TurnOutcome,
@@ -510,7 +519,7 @@ async fn record_call(
})
}
enum PreExecution {
pub(crate) enum PreExecution {
Run(Arc<dyn crate::tool::Tool>),
Resolved(CallOutcome),
TurnCancelled,
@@ -519,8 +528,9 @@ enum PreExecution {
Suspended,
}
/// Gate + hooks.pre + tool lookup — shared by sequential and fan-out paths.
async fn pre_execution(
/// Gate + hooks.pre + tool lookup — shared by the sequential path, the
/// fan-out and [`crate::recovery`]'s re-execution of an interrupted call.
pub(crate) async fn pre_execution(
deps: &Arc<KernelDeps>,
params: &LoopParams,
events: &EventSink,
@@ -572,8 +582,8 @@ async fn pre_execution(
}
}
/// Phase-3 shared by both paths: hooks.post → resolve → emit.
async fn record_outcome(
/// Phase-3 shared by both paths (and by recovery): hooks.post → resolve → emit.
pub(crate) async fn record_outcome(
deps: &Arc<KernelDeps>,
params: &LoopParams,
events: &EventSink,