1use std::collections::{BTreeMap, BTreeSet};
48
49use serde::Serialize;
50
51use super::checkpoint::{
52 AuthoredMemoryQueryState, AuthoredMemoryWriteState, ChildProcessState, ContextVmStateV1,
53 EntropyState, EntropyTurnState, HandleState, InlineMessageBody, KnowledgeSlotState,
54 LogicalCompressionEntry, LogicalKernelState, LogicalPlanStep, LogicalStateProjection,
55 LogicalTaskState, LogicalToolCall, MessagePartition, MilestoneState, PartitionTokenState,
56 PendingPayloadLoadState, PendingProviderCallState, QueuedSignalState, ReferencedMessageBody,
57 SchedulerStateV1, SkillLeaseState, StoredMessageBody, StoredMessageState,
58 StructuredMessageBody, SyscallStateV1, TaskAttemptState, TaskControlState, WorkflowGraphState,
59 WorkflowNodeState,
60};
61use super::command::{
62 ApplyCapabilityPatchCommand, ApplyKnowledgeMutationCommand, ApplyPolicyPatchCommand,
63 ApplySkillActivationCommand, CancelCommand, CancellationReason, HostCommand, LivePolicyState,
64 SeedKnowledgeCommand, TaskUpdate as WireTaskUpdate, UpdateDeadlineCommand, UpdateTaskCommand,
65};
66use super::config::ResolvedOperationConfig;
67use super::effect::{
68 ApprovalRequest as WireApprovalRequest, ArchivePageOutEffect, CallProviderEffect,
69 CanonicalMemoryQuery, CanonicalMemoryWrite, EffectKind, EffectKindTag, EffectOutcome,
70 EffectSuccess, EvaluateMilestoneEffect, ExecuteToolsEffect, HostEffectFailure, KernelEffect,
71 LaunchToken, LoadPayloadEffect, PageOutPayload, PayloadRef, PersistMemoryEffect,
72 PreemptTasksEffect, ProviderCompleted, ProviderMessage, ProviderOutcome, QueryMemoryEffect,
73 RenderedContext as WireRenderedContext, RequestApprovalEffect, SpawnTasksEffect,
74 TaskAttemptRef, TaskLaunch, ToolCall as WireToolCall, ToolResultDisposition,
75 ToolResultPayload as WireToolResultPayload, ToolSchema as WireToolSchema,
76 WorkflowBudget as WireWorkflowBudget,
77};
78use super::envelope::{OperationLifecycle, ResolveEffect};
79use super::event::{
80 ChildCompleted, ChildStatus, DeliverSignal, ExternalEvent, LogicalSignal, SignalSourceKind,
81 SignalTarget, SignalUrgency,
82};
83use super::fault::{KernelFault, KernelFaultCode};
84use super::record::NormalizedPayload;
85use super::root::{
86 AgentIsolation as WireIsolation, AgentRole as WireRole, ExecutionFocus, InitialContext,
87 LogicalAgentSpec, LogicalContextInheritance as WireContextInheritance, LogicalTask,
88 MessageRole, RootEntry, RootKind, WorkflowNode as WireNode, WorkflowSpec as WireSpec,
89};
90use super::scalar::{
91 AttemptId, EffectId, MemoryBindingId, NodeId, OperationId, TaskId, WireU64, WorkflowId,
92};
93use super::syscall::{
94 ChildAttemptCausation, MemoryKind as WireMemoryKind, ProviderToolCausation, SyscallCausation,
95 SyscallRequest,
96};
97use super::terminal::{
98 AgentTerminal, CancelledTerminal, EffectsDisposition, FailedTerminal, KernelFailure,
99 KernelFailureCode, KernelTerminal, LoopResult as WireLoopResult, StepDisposition,
100 TerminalDisposition, TerminationReason as WireTermination, UsageReport, WorkflowOutcome,
101 WorkflowStatus, WorkflowTerminal,
102};
103use super::transaction::{PlanContext, TransitionStep};
104
105use crate::context::manager::READ_RESULT_TOOL_NAME;
106use crate::context::task_state::{CompressionEntry, PlanStep, TaskState};
107use crate::mm::handle::{Handle, HandleKind, Residency};
108use crate::orchestration::task_graph::TaskStatus;
109use crate::orchestration::workflow::run::{WorkflowNodeStatus, WorkflowRuntimeNodeState};
110use crate::orchestration::workflow::{
111 WorkflowNode as CoreWorkflowNode, WorkflowSpec as CoreWorkflowSpec,
112};
113use crate::runtime::kernel::{KernelObservation, WorkflowSpawnFailure};
114use crate::scheduler::policy::SchedulerBudget;
115use crate::scheduler::state_machine::{
116 AdjudicatedTurn, AnsweredCall, IdleContinuation, LoopAction, LoopEvent, LoopStateMachine,
117};
118use crate::scheduler::tcb::{BudgetLedger, ProcInfo, TaskLifecycle, Tcb, WaitReason};
119use crate::signals::queue::QueuedSignalRuntimeState;
120use crate::signals::router::SignalRouterRuntimeState;
121use crate::syscall::{Disposition, Syscall as CoreSyscall};
122use crate::types::agent::{
123 AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec,
124 ContextInheritance, LoopRoundSpec,
125};
126use crate::types::message::{Content, ContentPart, Message, Role, ToolErrorKind, ToolResult};
127use crate::types::result::{
128 LoopResult, PaceAction as CorePaceAction, SubAgentResult, TerminationReason,
129};
130use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
131use crate::types::task::{RuntimeTask, TaskLane};
132
133#[derive(Debug, Clone, Serialize)]
150pub struct PlannedStep {
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub root_kind: Option<RootKind>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub focus: Option<ExecutionFocus>,
155 #[serde(skip_serializing_if = "Vec::is_empty")]
156 pub observations: Vec<KernelObservation>,
157 pub disposition: StepDisposition,
158}
159
160impl PartialEq for PlannedStep {
161 fn eq(&self, other: &Self) -> bool {
162 self.root_kind == other.root_kind
163 && self.focus == other.focus
164 && self.disposition == other.disposition
165 && serde_json::to_vec(&self.observations).ok()
166 == serde_json::to_vec(&other.observations).ok()
167 }
168}
169
170impl PlannedStep {
171 fn quiet(root_kind: Option<RootKind>, focus: Option<ExecutionFocus>) -> Self {
172 Self {
173 root_kind,
174 focus,
175 observations: Vec::new(),
176 disposition: StepDisposition::Effects(EffectsDisposition::default()),
177 }
178 }
179}
180
181impl TransitionStep for PlannedStep {
182 fn disposition(&self) -> &StepDisposition {
183 &self.disposition
184 }
185}
186
187pub const ROOT_TASK_ID: &str = "root";
194
195const NO_HOST_SESSION: &str = "";
200
201#[derive(Debug, Clone, PartialEq)]
202struct StagedFocus {
203 step_seq: WireU64,
204 root_kind: Option<RootKind>,
205 focus: Option<ExecutionFocus>,
206}
207
208#[derive(Debug, Clone, PartialEq)]
218struct PendingProviderCall {
219 task_id: TaskId,
220 exposed_tools: BTreeSet<String>,
221}
222
223#[derive(Debug, Clone, PartialEq)]
231struct SyscallRejection {
232 operation: &'static str,
233 subject: Option<String>,
236 reason: String,
237}
238
239impl SyscallRejection {
240 fn new(operation: &'static str, reason: impl Into<String>) -> Self {
241 Self {
242 operation,
243 subject: None,
244 reason: reason.into(),
245 }
246 }
247
248 fn by(mut self, caller: &TaskId) -> Self {
249 self.subject = Some(caller.as_str().to_string());
250 self
251 }
252}
253
254#[derive(Debug, Clone)]
256enum SyscallRefusal {
257 Fault(KernelFault),
260 Rejected(SyscallRejection),
263}
264
265fn handle_kind_label(kind: &HandleKind) -> &'static str {
271 match kind {
272 HandleKind::ToolResult => "tool_result",
273 HandleKind::MemoryPage => "memory_page",
274 HandleKind::KnowledgeEntry => "knowledge_entry",
275 HandleKind::SubAgentJoin => "sub_agent_join",
276 }
277}
278
279fn role_label(role: Role) -> &'static str {
280 match role {
281 Role::System => "system",
282 Role::User => "user",
283 Role::Assistant => "assistant",
284 Role::Tool => "tool",
285 }
286}
287
288fn role_from_label(label: &str) -> Option<Role> {
289 match label {
290 "system" => Some(Role::System),
291 "user" => Some(Role::User),
292 "assistant" => Some(Role::Assistant),
293 "tool" => Some(Role::Tool),
294 _ => None,
295 }
296}
297
298#[allow(clippy::type_complexity)]
304fn message_body_parts(message: &Message) -> Option<(String, Option<String>, bool)> {
305 match &message.content {
306 Content::Text(text) => Some((text.clone(), None, false)),
307 Content::Parts(parts) => {
308 let mut text = String::new();
309 let mut tool_call_id = None;
310 let mut is_error = false;
311 for part in parts {
312 match part {
313 ContentPart::Text { text: chunk } => text.push_str(chunk),
314 ContentPart::ToolResult {
315 call_id,
316 output,
317 is_error: failed,
318 } => {
319 if tool_call_id.is_some() {
320 return None;
323 }
324 tool_call_id = Some(call_id.to_string());
325 text.push_str(output);
326 is_error = *failed;
327 }
328 ContentPart::Image { .. } | ContentPart::Audio { .. } => return None,
329 }
330 }
331 Some((text, tool_call_id, is_error))
332 }
333 }
334}
335
336fn message_content(text: String, tool_call_id: Option<&str>, is_error: bool) -> Content {
341 match tool_call_id {
342 Some(call_id) => Content::Parts(vec![ContentPart::ToolResult {
343 call_id: call_id.into(),
344 output: text,
345 is_error,
346 }]),
347 None => Content::Text(text),
348 }
349}
350
351fn workflow_kind_label(state: &WorkflowRuntimeNodeState) -> &'static str {
352 match state.node.kind {
353 crate::orchestration::workflow::NodeKind::Spawn => "spawn",
354 crate::orchestration::workflow::NodeKind::Loop { .. } => "loop",
355 crate::orchestration::workflow::NodeKind::Classify { .. } => "classify",
356 crate::orchestration::workflow::NodeKind::Tournament { .. } => "tournament",
357 crate::orchestration::workflow::NodeKind::Reduce { .. } => "reduce",
358 }
359}
360
361fn workflow_status_label(status: TaskStatus) -> &'static str {
362 match status {
363 TaskStatus::Pending => "pending",
364 TaskStatus::Ready => "ready",
365 TaskStatus::Running => "running",
366 TaskStatus::Completed => "completed",
367 TaskStatus::CompletedPartial => "completed_partial",
368 TaskStatus::Failed => "failed",
369 TaskStatus::SkippedUpstreamFailed => "skipped_upstream_failed",
370 }
371}
372
373fn restore_workflow_status(label: &str) -> Result<TaskStatus, KernelFault> {
374 match label {
375 "pending" => Ok(TaskStatus::Pending),
376 "ready" => Ok(TaskStatus::Ready),
377 "running" => Ok(TaskStatus::Running),
378 "completed" => Ok(TaskStatus::Completed),
379 "completed_partial" => Ok(TaskStatus::CompletedPartial),
380 "failed" => Ok(TaskStatus::Failed),
381 "skipped_upstream_failed" => Ok(TaskStatus::SkippedUpstreamFailed),
382 other => Err(KernelFault::new(
383 KernelFaultCode::CheckpointIncompatible,
384 format!("workflow checkpoint carries unknown node status {other:?}"),
385 )),
386 }
387}
388
389fn agent_role_label(role: AgentRole) -> &'static str {
390 match role {
391 AgentRole::Explore => "explore",
392 AgentRole::Plan => "plan",
393 AgentRole::Implement => "implement",
394 AgentRole::Verify => "verify",
395 AgentRole::Custom => "custom",
396 }
397}
398
399fn restore_agent_role(label: &str) -> Result<AgentRole, KernelFault> {
400 match label {
401 "explore" => Ok(AgentRole::Explore),
402 "plan" => Ok(AgentRole::Plan),
403 "implement" => Ok(AgentRole::Implement),
404 "verify" => Ok(AgentRole::Verify),
405 "custom" => Ok(AgentRole::Custom),
406 other => Err(KernelFault::new(
407 KernelFaultCode::CheckpointIncompatible,
408 format!("child process carries unknown role {other:?}"),
409 )),
410 }
411}
412
413fn agent_isolation_label(isolation: AgentIsolation) -> &'static str {
414 match isolation {
415 AgentIsolation::Shared => "shared",
416 AgentIsolation::ReadOnly => "read_only",
417 AgentIsolation::Worktree => "worktree",
418 AgentIsolation::Remote => "remote",
419 }
420}
421
422fn restore_agent_isolation(label: &str) -> Result<AgentIsolation, KernelFault> {
423 match label {
424 "shared" => Ok(AgentIsolation::Shared),
425 "read_only" => Ok(AgentIsolation::ReadOnly),
426 "worktree" => Ok(AgentIsolation::Worktree),
427 "remote" => Ok(AgentIsolation::Remote),
428 other => Err(KernelFault::new(
429 KernelFaultCode::CheckpointIncompatible,
430 format!("child process carries unknown isolation {other:?}"),
431 )),
432 }
433}
434
435fn context_inheritance_label(inheritance: ContextInheritance) -> &'static str {
436 match inheritance {
437 ContextInheritance::None => "none",
438 ContextInheritance::SystemOnly => "system_only",
439 ContextInheritance::Full => "full",
440 }
441}
442
443fn restore_context_inheritance(label: &str) -> Result<ContextInheritance, KernelFault> {
444 match label {
445 "none" => Ok(ContextInheritance::None),
446 "system_only" => Ok(ContextInheritance::SystemOnly),
447 "full" => Ok(ContextInheritance::Full),
448 other => Err(KernelFault::new(
449 KernelFaultCode::CheckpointIncompatible,
450 format!("child process carries unknown context inheritance {other:?}"),
451 )),
452 }
453}
454
455fn queued_signal_state(queued: &QueuedSignalRuntimeState) -> QueuedSignalState {
456 let signal = &queued.signal;
457 QueuedSignalState {
458 signal_id: super::scalar::SignalId::new(signal.id.as_str())
459 .expect("a canonical runtime signal keeps its branded id"),
460 source: signal_source_label(signal.source).to_string(),
461 signal_type: signal_type_label(signal.signal_type).to_string(),
462 urgency: urgency_label(signal.urgency).to_string(),
463 summary: signal.summary.to_string(),
464 payload: super::scalar::BoundedJson::new(signal.payload.clone())
465 .expect("a canonical signal payload remains bounded"),
466 dedupe_key: signal.dedupe_key.as_ref().map(ToString::to_string),
467 deadline_ms: signal.deadline_ms.map(WireU64::new),
468 coalesce_key: signal.coalesce_key.as_ref().map(ToString::to_string),
469 coalesced_count: signal.coalesced_count,
470 recipient: signal.recipient.as_ref().map(ToString::to_string),
471 timestamp_ms: WireU64::new(signal.timestamp_ms),
472 deadline_escalated: queued.deadline_escalated,
473 dedupe_keys: queued.dedupe_keys.iter().map(ToString::to_string).collect(),
474 }
475}
476
477fn restore_queued_signal(
478 queued: &QueuedSignalState,
479) -> Result<QueuedSignalRuntimeState, KernelFault> {
480 if queued.coalesced_count == 0 {
481 return Err(KernelFault::new(
482 KernelFaultCode::CheckpointIncompatible,
483 format!(
484 "queued signal {} carries a zero coalesced count",
485 queued.signal_id
486 ),
487 ));
488 }
489 Ok(QueuedSignalRuntimeState {
490 signal: RuntimeSignal {
491 id: queued.signal_id.as_str().into(),
492 source: restore_signal_source(&queued.source)?,
493 signal_type: restore_signal_type(&queued.signal_type)?,
494 urgency: restore_urgency(&queued.urgency)?,
495 summary: queued.summary.as_str().into(),
496 payload: queued.payload.get().clone(),
497 dedupe_key: queued.dedupe_key.as_deref().map(Into::into),
498 deadline_ms: queued.deadline_ms.map(WireU64::get),
499 coalesce_key: queued.coalesce_key.as_deref().map(Into::into),
500 coalesced_count: queued.coalesced_count,
501 recipient: queued.recipient.as_deref().map(Into::into),
502 timestamp_ms: queued.timestamp_ms.get(),
503 },
504 deadline_escalated: queued.deadline_escalated,
505 dedupe_keys: queued
506 .dedupe_keys
507 .iter()
508 .map(|key| key.as_str().into())
509 .collect(),
510 })
511}
512
513fn signal_source_label(source: SignalSource) -> &'static str {
514 match source {
515 SignalSource::Cron => "cron",
516 SignalSource::Gateway => "gateway",
517 SignalSource::Heartbeat => "heartbeat",
518 SignalSource::Custom => "custom",
519 }
520}
521
522fn restore_signal_source(label: &str) -> Result<SignalSource, KernelFault> {
523 match label {
524 "cron" => Ok(SignalSource::Cron),
525 "gateway" => Ok(SignalSource::Gateway),
526 "heartbeat" => Ok(SignalSource::Heartbeat),
527 "custom" => Ok(SignalSource::Custom),
528 other => Err(KernelFault::new(
529 KernelFaultCode::CheckpointIncompatible,
530 format!("queued signal carries unknown source {other:?}"),
531 )),
532 }
533}
534
535fn signal_type_label(signal_type: SignalType) -> &'static str {
536 match signal_type {
537 SignalType::Event => "event",
538 SignalType::Job => "job",
539 SignalType::Alert => "alert",
540 }
541}
542
543fn restore_signal_type(label: &str) -> Result<SignalType, KernelFault> {
544 match label {
545 "event" => Ok(SignalType::Event),
546 "job" => Ok(SignalType::Job),
547 "alert" => Ok(SignalType::Alert),
548 other => Err(KernelFault::new(
549 KernelFaultCode::CheckpointIncompatible,
550 format!("queued signal carries unknown type {other:?}"),
551 )),
552 }
553}
554
555fn urgency_label(urgency: Urgency) -> &'static str {
556 match urgency {
557 Urgency::Low => "low",
558 Urgency::Normal => "normal",
559 Urgency::High => "high",
560 Urgency::Critical => "critical",
561 }
562}
563
564fn restore_urgency(label: &str) -> Result<Urgency, KernelFault> {
565 match label {
566 "low" => Ok(Urgency::Low),
567 "normal" => Ok(Urgency::Normal),
568 "high" => Ok(Urgency::High),
569 "critical" => Ok(Urgency::Critical),
570 other => Err(KernelFault::new(
571 KernelFaultCode::CheckpointIncompatible,
572 format!("queued signal carries unknown urgency {other:?}"),
573 )),
574 }
575}
576
577fn restore_scheduler(
579 engine: &mut LoopStateMachine,
580 config: &ResolvedOperationConfig,
581 state: &SchedulerStateV1,
582) -> Result<(), KernelFault> {
583 engine.turn = state.turn;
584 engine.restore_budget_usage(state.total_tokens.get(), state.rounds_completed);
585 engine.restore_started_at_ms(state.started_at_ms.map(WireU64::get));
586 engine.set_wall_budget(state.wall_budget_ms.map(WireU64::get));
587 engine
588 .restore_entropy_checkpoint_state(crate::scheduler::entropy::EntropyTrackerRuntimeState {
589 window: state
590 .entropy
591 .window
592 .iter()
593 .map(|entry| crate::scheduler::entropy::EntropyTurnRuntimeState {
594 errored_results: entry.errored_results,
595 total_results: entry.total_results,
596 rollbacks: entry.rollbacks,
597 })
598 .collect(),
599 rollbacks_pending: state.entropy.rollbacks_pending,
600 disarmed: state.entropy.disarmed,
601 last_alert_turn: state.entropy.last_alert_turn,
602 })
603 .map_err(|error| {
604 KernelFault::new(
605 KernelFaultCode::CheckpointIncompatible,
606 format!("entropy checkpoint could not be rebuilt: {error}"),
607 )
608 })?;
609
610 let limits = SchedulerBudget {
611 max_tokens: config.execution_policy.max_context_tokens,
612 max_turns: config.execution_policy.max_turns,
613 max_total_tokens: config.execution_policy.max_total_tokens.get(),
614 max_wall_ms: state.wall_budget_ms.map(WireU64::get),
615 };
616 let table = engine.task_table_mut();
617 for task in &state.tasks {
618 let mut tcb = Tcb::root(task.task_id.as_str(), limits.clone());
619 tcb.parent = task
620 .parent_task_id
621 .as_ref()
622 .map(|parent| parent.as_str().into());
623 tcb.state = restore_task_lifecycle(task)?;
624 tcb.wait = restore_task_wait(task)?;
625 tcb.caps = task.capability_ids.iter().map(|cap| cap.into()).collect();
626 tcb.proc = task
627 .process
628 .as_ref()
629 .map(|process| {
630 let result = process
631 .join_result
632 .as_ref()
633 .map(|value| {
634 serde_json::from_value(value.get().clone()).map_err(|error| {
635 KernelFault::new(
636 KernelFaultCode::CheckpointIncompatible,
637 format!(
638 "task {} carries an invalid child join result: {error}",
639 task.task_id
640 ),
641 )
642 })
643 })
644 .transpose()?;
645 if result.as_ref().is_some_and(|result: &SubAgentResult| {
646 result.agent_id.as_str() != task.task_id.as_str()
647 }) {
648 return Err(KernelFault::new(
649 KernelFaultCode::CheckpointIncompatible,
650 format!(
651 "task {} carries a join result for another child",
652 task.task_id
653 ),
654 ));
655 }
656 Ok(ProcInfo {
657 role: restore_agent_role(&process.role)?,
658 isolation: restore_agent_isolation(&process.isolation)?,
659 context_inheritance: restore_context_inheritance(&process.context_inheritance)?,
660 result,
661 })
662 })
663 .transpose()?;
664 tcb.budget = BudgetLedger {
665 limits: limits.clone(),
666 turns: task.turns_used,
667 total_tokens: task.tokens_used.get(),
668 started_at_ms: state.started_at_ms.map(WireU64::get),
669 };
670 table.insert(tcb);
671 }
672
673 let queued = state
674 .queued_signals
675 .iter()
676 .map(restore_queued_signal)
677 .collect::<Result<Vec<_>, _>>()?;
678 engine
679 .restore_signal_checkpoint_state(SignalRouterRuntimeState {
680 queued,
681 seen_order: state
682 .signal_dedupe_keys
683 .iter()
684 .map(|key| key.as_str().into())
685 .collect(),
686 })
687 .map_err(|error| {
688 KernelFault::new(
689 KernelFaultCode::CheckpointIncompatible,
690 format!("signal checkpoint could not be rebuilt: {error}"),
691 )
692 })?;
693
694 if let Some(workflow) = &state.workflow {
695 let wire_spec = WireSpec {
696 name: String::new(),
697 nodes: workflow
698 .nodes
699 .iter()
700 .map(|node| WireNode {
701 node_id: node.node_id.clone(),
702 task: node.task.clone(),
703 depends_on: node.depends_on.clone(),
704 run_spec: node.run_spec.clone(),
705 })
706 .collect(),
707 };
708 let core_spec = build_core_spec(&wire_spec).map_err(|fault| {
709 KernelFault::new(KernelFaultCode::CheckpointIncompatible, fault.message)
710 })?;
711 let runtime_states: Result<Vec<_>, KernelFault> = workflow
712 .nodes
713 .iter()
714 .enumerate()
715 .zip(core_spec.nodes.iter())
716 .map(|((index, node), core)| {
717 if node.kind != "spawn" {
718 return Err(KernelFault::new(
719 KernelFaultCode::CheckpointIncompatible,
720 format!(
721 "workflow node {} carries unsupported checkpoint kind {:?}",
722 node.node_id, node.kind
723 ),
724 ));
725 }
726 let result = engine
727 .task_table()
728 .get(&crate::orchestration::workflow::node_agent_id(index))
729 .and_then(|task| task.proc.as_ref())
730 .and_then(|process| process.result.as_ref())
731 .map(|result| result.result.clone());
732 Ok(WorkflowRuntimeNodeState {
733 node: core.clone(),
734 status: restore_workflow_status(&node.status)?,
735 result,
736 active_agent_id: node.active_agent_id.clone(),
737 iterations_completed: node.iterations_completed as usize,
738 })
739 })
740 .collect();
741 let run = crate::orchestration::workflow::WorkflowRun::restore_from_checkpoint(
742 &core_spec,
743 &runtime_states?,
744 )
745 .map_err(|error| {
746 KernelFault::new(
747 KernelFaultCode::CheckpointIncompatible,
748 format!("workflow checkpoint could not be rebuilt: {error}"),
749 )
750 })?;
751 engine.restore_checkpoint_workflow(run);
752 }
753 Ok(())
754}
755
756fn restore_task_lifecycle(task: &TaskControlState) -> Result<TaskLifecycle, KernelFault> {
757 let lifecycle = match task.lifecycle.as_str() {
758 "pending_launch" => TaskLifecycle::PendingLaunch,
759 "starting" => TaskLifecycle::Starting,
760 "ready" => TaskLifecycle::Ready,
761 "running" => TaskLifecycle::Running,
762 "suspended" => TaskLifecycle::Suspended,
763 "done" => {
764 let label = task.termination.as_deref().ok_or_else(|| {
765 incompatible(format!(
766 "task {} is done but the checkpoint does not say why; a finished task without \
767 its termination reason is not restorable",
768 task.task_id
769 ))
770 })?;
771 TaskLifecycle::Done(termination_from_label(label).ok_or_else(|| {
772 incompatible(format!(
773 "task {} names termination reason {label:?}, which this kernel does not know",
774 task.task_id
775 ))
776 })?)
777 }
778 other => {
779 return Err(incompatible(format!(
780 "task {} names lifecycle {other:?}, which this kernel does not know",
781 task.task_id
782 )));
783 }
784 };
785 Ok(lifecycle)
786}
787
788fn restore_task_wait(task: &TaskControlState) -> Result<Option<WaitReason>, KernelFault> {
789 match task.wait.as_deref() {
790 None => Ok(None),
791 Some("approval") => Ok(Some(WaitReason::Approval)),
792 Some("sub_agent_join") => Ok(Some(WaitReason::SubAgentJoin(
793 task.waiting_on
794 .iter()
795 .map(|child| child.as_str().into())
796 .collect(),
797 ))),
798 Some(other) => Err(incompatible(format!(
799 "task {} waits on {other:?}, which this kernel does not know",
800 task.task_id
801 ))),
802 }
803}
804
805fn termination_from_label(label: &str) -> Option<TerminationReason> {
806 Some(match label {
807 "completed" => TerminationReason::Completed,
808 "max_turns" => TerminationReason::MaxTurns,
809 "token_budget" => TerminationReason::TokenBudget,
810 "timeout" => TerminationReason::Timeout,
811 "user_abort" => TerminationReason::UserAbort,
812 "error" => TerminationReason::Error,
813 "milestone_exceeded" => TerminationReason::MilestoneExceeded,
814 "context_overflow" => TerminationReason::ContextOverflow,
815 "no_progress" => TerminationReason::NoProgress,
816 _ => return None,
817 })
818}
819
820fn restore_context_vm(
826 engine: &mut LoopStateMachine,
827 state: &ContextVmStateV1,
828) -> Result<(), KernelFault> {
829 let ctx = &mut engine.ctx;
830 for entry in &state.messages {
831 let message = restore_message(&entry.role, &entry.body, &entry.tool_calls)?;
832 match entry.partition {
833 MessagePartition::System => ctx.partitions.system.push(message, entry.tokens),
834 MessagePartition::History => ctx.partitions.history.push(message, entry.tokens),
835 }
836 }
837 for slot in &state.knowledge {
838 let message = restore_message(&slot.role, &slot.body, &[])?;
839 ctx.partitions.knowledge.push_entry(
840 slot.key.as_deref().map(Into::into),
841 message,
842 slot.tokens,
843 slot.pinned,
844 );
845 if slot.evict_at_boundary
849 && let Some(entry) = ctx.partitions.knowledge.entries.last_mut()
850 {
851 entry.evict_at_boundary = true;
852 }
853 }
854 ctx.partitions.signals = state.signals.clone();
855 ctx.partitions.task_state = restore_task_state(&state.task_state);
856 ctx.last_activity_ms = state.last_activity_ms.get();
857 ctx.last_compact_ms = state.last_compact_ms.map(WireU64::get);
858 ctx.active_skills = state
859 .active_skills
860 .iter()
861 .map(|lease| (lease.skill.as_str().into(), lease.lease_until_turn))
862 .collect();
863
864 for handle in &state.handles {
865 ctx.handles.insert(Handle {
866 id: handle.handle_id,
867 kind: restore_handle_kind(&handle.kind)?,
868 residency: restore_residency(handle)?,
869 tokens: handle.tokens,
870 source: handle.source.as_deref().map(Into::into),
871 });
872 }
873 ctx.restore_next_handle_id(state.next_handle_id);
874 if !ctx.restore_frozen_history_len(state.frozen_history_len as usize) {
875 return Err(incompatible(format!(
876 "the checkpoint freezes {} history messages but restores only {}",
877 state.frozen_history_len,
878 ctx.partitions.history.messages.len()
879 )));
880 }
881 Ok(())
882}
883
884fn restore_message(
885 role: &str,
886 body: &StoredMessageBody,
887 tool_calls: &[LogicalToolCall],
888) -> Result<Message, KernelFault> {
889 let role = role_from_label(role)
890 .ok_or_else(|| incompatible(format!("the checkpoint carries message role {role:?}")))?;
891 let content = match body {
892 StoredMessageBody::Inline(inline) => message_content(
893 inline.text.clone(),
894 inline.tool_call_id.as_deref(),
895 inline.is_error,
896 ),
897 StoredMessageBody::Reference(reference) => message_content(
898 reference.preview.clone(),
899 reference.tool_call_id.as_deref(),
900 reference.is_error,
901 ),
902 StoredMessageBody::Structured(structured) => serde_json::from_str(&structured.content_json)
903 .map_err(|error| {
904 incompatible(format!(
905 "the checkpoint carries a structured message body that does not decode: {error}"
906 ))
907 })?,
908 };
909 Ok(Message {
910 role,
911 content,
912 tool_calls: tool_calls
913 .iter()
914 .map(|call| {
915 Ok(crate::types::message::ToolCall {
916 id: call.call_id.as_str().into(),
917 name: call.name.as_str().into(),
918 arguments: serde_json::from_str(&call.arguments).map_err(|error| {
919 incompatible(format!(
920 "tool call {} carries arguments that do not decode: {error}",
921 call.call_id
922 ))
923 })?,
924 })
925 })
926 .collect::<Result<Vec<_>, KernelFault>>()?,
927 token_count: None,
928 })
929}
930
931fn restore_handle_kind(label: &str) -> Result<HandleKind, KernelFault> {
932 Ok(match label {
933 "tool_result" => HandleKind::ToolResult,
934 "memory_page" => HandleKind::MemoryPage,
935 "knowledge_entry" => HandleKind::KnowledgeEntry,
936 "sub_agent_join" => HandleKind::SubAgentJoin,
937 other => {
938 return Err(incompatible(format!(
939 "the checkpoint carries handle kind {other:?}, which this kernel does not know"
940 )));
941 }
942 })
943}
944
945fn restore_residency(handle: &HandleState) -> Result<Residency, KernelFault> {
946 let missing = |what: &str| {
947 incompatible(format!(
948 "handle {} is {} but carries no {what}",
949 handle.handle_id, handle.residency
950 ))
951 };
952 Ok(match handle.residency.as_str() {
953 "resident" => Residency::Resident,
954 "collapsed" => Residency::Collapsed,
955 "external" => Residency::External {
956 payload_ref: handle
957 .payload_ref
958 .clone()
959 .ok_or_else(|| missing("locator"))?,
960 digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
961 original_size: handle
962 .original_size
963 .ok_or_else(|| missing("original size"))?
964 .get(),
965 },
966 "paged_out" => Residency::PagedOut {
967 payload_ref: handle
968 .payload_ref
969 .clone()
970 .ok_or_else(|| missing("locator"))?,
971 digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
972 },
973 other => {
974 return Err(incompatible(format!(
975 "the checkpoint carries residency {other:?}, which this kernel does not know"
976 )));
977 }
978 })
979}
980
981fn incompatible(message: String) -> KernelFault {
982 KernelFault::new(KernelFaultCode::CheckpointIncompatible, message)
983}
984
985fn project_task_state(state: &TaskState) -> LogicalTaskState {
986 LogicalTaskState {
987 goal: state.goal.clone(),
988 criteria: state.criteria.clone(),
989 plan: state
990 .plan
991 .iter()
992 .map(|step| LogicalPlanStep {
993 label: step.label.clone(),
994 done: step.done,
995 })
996 .collect(),
997 current_step: state.current_step.map(|index| index as u32),
998 progress: state.progress.clone(),
999 scratchpad: state.scratchpad.clone(),
1000 blocked_on: state.blocked_on.clone(),
1001 directives: state.directives.clone(),
1002 preserved_refs: state.preserved_refs.clone(),
1003 recent_actions: state.recent_actions.clone(),
1004 compression_log: state
1005 .compression_log
1006 .iter()
1007 .map(|entry| LogicalCompressionEntry {
1008 action: entry.action.clone(),
1009 summary: entry.summary.clone(),
1010 })
1011 .collect(),
1012 compression_log_dropped: WireU64::new(state.compression_log_dropped),
1013 }
1014}
1015
1016fn restore_task_state(state: &LogicalTaskState) -> TaskState {
1017 TaskState {
1018 goal: state.goal.clone(),
1019 criteria: state.criteria.clone(),
1020 plan: state
1021 .plan
1022 .iter()
1023 .map(|step| PlanStep {
1024 label: step.label.clone(),
1025 done: step.done,
1026 })
1027 .collect(),
1028 current_step: state.current_step.map(|index| index as usize),
1029 progress: state.progress.clone(),
1030 scratchpad: state.scratchpad.clone(),
1031 blocked_on: state.blocked_on.clone(),
1032 directives: state.directives.clone(),
1033 preserved_refs: state.preserved_refs.clone(),
1034 recent_actions: state.recent_actions.clone(),
1035 compression_log: state
1036 .compression_log
1037 .iter()
1038 .map(|entry| CompressionEntry {
1039 action: entry.action.clone(),
1040 summary: entry.summary.clone(),
1041 })
1042 .collect(),
1043 compression_log_dropped: state.compression_log_dropped.get(),
1044 }
1045}
1046
1047fn authority(message: &str) -> SyscallRefusal {
1048 SyscallRefusal::Fault(KernelFault::new(
1049 KernelFaultCode::InvalidAuthority,
1050 message.to_string(),
1051 ))
1052}
1053
1054fn denial_reason(disposition: &Disposition, fallback: &str) -> String {
1055 match disposition {
1056 Disposition::Deny { stage, reason } => format!("{stage}: {reason}"),
1057 Disposition::RateLimited { retry_after_ms } => {
1058 format!("rate limited; retry after {retry_after_ms}ms")
1059 }
1060 Disposition::Gate { reason, .. } => format!("awaiting approval: {reason}"),
1061 Disposition::Defer { slot } => format!("deferred at slot {slot}"),
1062 Disposition::Allow => fallback.to_string(),
1063 }
1064}
1065
1066#[derive(Debug, Clone, PartialEq)]
1068struct AuthoredMemoryWrite {
1069 binding_id: MemoryBindingId,
1070 name: String,
1071 kind: WireMemoryKind,
1072 size_bytes: u32,
1073}
1074
1075#[derive(Debug, Clone, PartialEq)]
1077struct AuthoredMemoryQuery {
1078 binding_id: MemoryBindingId,
1079 text: String,
1080 requested_k: u32,
1081}
1082
1083#[derive(Debug, Clone, PartialEq)]
1085struct PendingPayloadLoad {
1086 handle_id: String,
1089 digest: String,
1092 original_size: Option<u64>,
1095}
1096
1097#[derive(Debug, Default)]
1099struct SyscallOutcome {
1100 effects: Vec<KernelEffect>,
1101 focus: Option<ExecutionFocus>,
1103 needs_workflow_round: bool,
1106}
1107
1108pub struct CanonicalOperationDriver {
1119 engine: Option<LoopStateMachine>,
1120 root_kind: Option<RootKind>,
1121 focus: Option<ExecutionFocus>,
1122 workflow_id: Option<WorkflowId>,
1123 node_ids: Vec<NodeId>,
1126 workflow_nodes: Vec<WireNode>,
1129 attempts: BTreeMap<String, AttemptId>,
1135 provider_calls: BTreeMap<EffectId, PendingProviderCall>,
1138 pending_memory_writes: BTreeMap<EffectId, AuthoredMemoryWrite>,
1143 pending_memory_queries: BTreeMap<EffectId, AuthoredMemoryQuery>,
1145 pending_payload_loads: BTreeMap<EffectId, PendingPayloadLoad>,
1149 consumed_calls: BTreeSet<String>,
1152 policy: Option<LivePolicyState>,
1157 loaded_contract_id: Option<String>,
1165 staged: Option<StagedFocus>,
1166 poison: Option<KernelFault>,
1167}
1168
1169impl Default for CanonicalOperationDriver {
1170 fn default() -> Self {
1171 Self::new()
1172 }
1173}
1174
1175impl CanonicalOperationDriver {
1176 pub fn new() -> Self {
1177 Self {
1178 engine: None,
1179 root_kind: None,
1180 focus: None,
1181 workflow_id: None,
1182 node_ids: Vec::new(),
1183 workflow_nodes: Vec::new(),
1184 attempts: BTreeMap::new(),
1185 provider_calls: BTreeMap::new(),
1186 pending_memory_writes: BTreeMap::new(),
1187 pending_memory_queries: BTreeMap::new(),
1188 pending_payload_loads: BTreeMap::new(),
1189 consumed_calls: BTreeSet::new(),
1190 policy: None,
1191 loaded_contract_id: None,
1192 staged: None,
1193 poison: None,
1194 }
1195 }
1196
1197 pub fn root_kind(&self) -> Option<RootKind> {
1201 self.root_kind
1202 }
1203
1204 pub fn focus(&self) -> Option<&ExecutionFocus> {
1206 self.focus.as_ref()
1207 }
1208
1209 pub fn workflow_id(&self) -> Option<&WorkflowId> {
1210 self.workflow_id.as_ref()
1211 }
1212
1213 pub fn attempt_id(&self, task_id: &str) -> Option<&AttemptId> {
1218 self.attempts.get(task_id)
1219 }
1220
1221 pub fn poison(&self) -> Option<&KernelFault> {
1222 self.poison.as_ref()
1223 }
1224
1225 pub fn engine(&self) -> Option<&LoopStateMachine> {
1227 self.engine.as_ref()
1228 }
1229
1230 pub fn lifecycle(&self) -> OperationLifecycle {
1233 match (self.engine.is_some(), self.root_kind) {
1234 (false, _) => OperationLifecycle::Created,
1235 (true, None) => OperationLifecycle::Configured,
1236 (true, Some(_)) => OperationLifecycle::Running,
1237 }
1238 }
1239
1240 pub fn project_logical_state(&self) -> LogicalStateProjection {
1253 LogicalStateProjection {
1254 root_kind: self.root_kind,
1255 focus: self.focus.clone(),
1256 syscall: self.project_syscall_state(),
1257 scheduler: self.project_scheduler_state(),
1258 context_vm: self.project_context_vm_state(),
1259 }
1260 }
1261
1262 fn project_syscall_state(&self) -> SyscallStateV1 {
1263 SyscallStateV1 {
1264 policy_revision: self.policy.as_ref().map(LivePolicyState::revision),
1265 live_config: self.policy.as_ref().map(|policy| policy.config().clone()),
1266 provider_calls: self
1267 .provider_calls
1268 .iter()
1269 .map(|(effect_id, call)| PendingProviderCallState {
1270 effect_id: effect_id.clone(),
1271 task_id: call.task_id.clone(),
1272 exposed_tools: call.exposed_tools.iter().cloned().collect(),
1273 })
1274 .collect(),
1275 consumed_call_ids: self.consumed_calls.iter().cloned().collect(),
1276 authored_memory_writes: self
1277 .pending_memory_writes
1278 .iter()
1279 .map(|(effect_id, write)| AuthoredMemoryWriteState {
1280 effect_id: effect_id.clone(),
1281 binding_id: write.binding_id.clone(),
1282 name: write.name.clone(),
1283 kind: write.kind,
1284 size_bytes: write.size_bytes,
1285 })
1286 .collect(),
1287 authored_memory_queries: self
1288 .pending_memory_queries
1289 .iter()
1290 .map(|(effect_id, query)| AuthoredMemoryQueryState {
1291 effect_id: effect_id.clone(),
1292 binding_id: query.binding_id.clone(),
1293 text: query.text.clone(),
1294 requested_k: query.requested_k,
1295 })
1296 .collect(),
1297 memory_write_window_ms: self
1298 .engine
1299 .as_ref()
1300 .map(|engine| {
1301 engine
1302 .memory_write_window()
1303 .iter()
1304 .copied()
1305 .map(WireU64::new)
1306 .collect()
1307 })
1308 .unwrap_or_default(),
1309 }
1310 }
1311
1312 fn project_scheduler_state(&self) -> SchedulerStateV1 {
1313 let Some(engine) = self.engine.as_ref() else {
1314 return SchedulerStateV1::default();
1315 };
1316 let (total_tokens, subagents_spawned, rounds_completed) = engine.local_budget_usage();
1317 let signal_state = engine.signal_checkpoint_state();
1318 let entropy_state = engine.entropy_checkpoint_state();
1319 let workflow = engine.workflow_checkpoint_nodes().map(|runtime_nodes| {
1320 let workflow_id = self
1321 .workflow_id
1322 .as_ref()
1323 .expect("an active canonical workflow has a logical identity");
1324 assert_eq!(
1325 runtime_nodes.len(),
1326 self.workflow_nodes.len(),
1327 "the semantic workflow and its canonical source DAG stay index-aligned"
1328 );
1329 WorkflowGraphState {
1330 workflow_id: workflow_id.clone(),
1331 nodes: runtime_nodes
1332 .into_iter()
1333 .zip(self.workflow_nodes.iter())
1334 .map(|(runtime, wire)| WorkflowNodeState {
1335 node_id: wire.node_id.clone(),
1336 task: wire.task.clone(),
1337 depends_on: wire.depends_on.clone(),
1338 run_spec: wire.run_spec.clone(),
1339 kind: workflow_kind_label(&runtime).to_string(),
1340 status: workflow_status_label(runtime.status).to_string(),
1341 active_agent_id: runtime.active_agent_id,
1342 iterations_completed: runtime.iterations_completed as u32,
1343 })
1344 .collect(),
1345 }
1346 });
1347 SchedulerStateV1 {
1348 turn: engine.turn,
1349 total_tokens: WireU64::new(total_tokens),
1350 rounds_completed,
1351 subagents_spawned,
1352 started_at_ms: engine.started_at_ms().map(WireU64::new),
1353 wall_budget_ms: engine.wall_budget().map(WireU64::new),
1354 tasks: engine
1355 .task_table()
1356 .all()
1357 .iter()
1358 .map(|tcb| TaskControlState {
1359 task_id: TaskId::new(tcb.id.as_str())
1360 .expect("an internal task id is always a legal branded ref"),
1361 parent_task_id: tcb
1362 .parent
1363 .as_ref()
1364 .and_then(|parent| TaskId::new(parent.as_str()).ok()),
1365 lifecycle: tcb.state.label().to_string(),
1366 termination: match tcb.state {
1367 TaskLifecycle::Done(reason) => Some(reason.label().to_string()),
1368 _ => None,
1369 },
1370 wait: tcb.wait.as_ref().map(|wait| wait.label().to_string()),
1371 waiting_on: match &tcb.wait {
1372 Some(WaitReason::SubAgentJoin(children)) => children
1373 .iter()
1374 .filter_map(|child| TaskId::new(child.as_str()).ok())
1375 .collect(),
1376 _ => Vec::new(),
1377 },
1378 capability_ids: tcb.caps.iter().map(|cap| cap.to_string()).collect(),
1379 process: tcb.proc.as_ref().map(|process| ChildProcessState {
1380 role: agent_role_label(process.role).to_string(),
1381 isolation: agent_isolation_label(process.isolation).to_string(),
1382 context_inheritance: context_inheritance_label(process.context_inheritance)
1383 .to_string(),
1384 join_result: process.result.as_ref().map(|result| {
1385 super::scalar::BoundedJson::new(
1386 serde_json::to_value(result)
1387 .expect("a child join result is serializable"),
1388 )
1389 .expect("a child join result is bounded")
1390 }),
1391 }),
1392 tokens_used: WireU64::new(tcb.budget.total_tokens),
1393 turns_used: tcb.budget.turns,
1394 })
1395 .collect(),
1396 attempts: self
1397 .attempts
1398 .iter()
1399 .filter_map(|(task_id, attempt_id)| {
1400 Some(TaskAttemptState {
1401 task_id: TaskId::new(task_id.as_str()).ok()?,
1402 attempt_id: attempt_id.clone(),
1403 })
1404 })
1405 .collect(),
1406 workflow,
1407 queued_signals: signal_state
1408 .queued
1409 .into_iter()
1410 .map(|queued| queued_signal_state(&queued))
1411 .collect(),
1412 signal_dedupe_keys: signal_state
1413 .seen_order
1414 .into_iter()
1415 .map(|key| key.to_string())
1416 .collect(),
1417 milestone: self
1418 .loaded_contract_id
1419 .as_ref()
1420 .map(|contract_id| MilestoneState {
1421 contract_id: contract_id.clone(),
1422 phase_id: engine.current_milestone_phase_id().map(str::to_string),
1423 complete: engine.is_milestone_complete(),
1424 blocked_count: engine.milestone_blocked_count(),
1425 }),
1426 entropy: EntropyState {
1427 window: entropy_state
1428 .window
1429 .into_iter()
1430 .map(|entry| EntropyTurnState {
1431 errored_results: entry.errored_results,
1432 total_results: entry.total_results,
1433 rollbacks: entry.rollbacks,
1434 })
1435 .collect(),
1436 rollbacks_pending: entropy_state.rollbacks_pending,
1437 disarmed: entropy_state.disarmed,
1438 last_alert_turn: entropy_state.last_alert_turn,
1439 },
1440 }
1441 }
1442
1443 fn project_context_vm_state(&self) -> ContextVmStateV1 {
1444 let Some(engine) = self.engine.as_ref() else {
1445 return ContextVmStateV1::default();
1446 };
1447 let ctx = &engine.ctx;
1448 ContextVmStateV1 {
1449 handles: ctx
1450 .handles
1451 .all()
1452 .iter()
1453 .map(|handle| {
1454 let (payload_ref, digest, original_size) = match &handle.residency {
1455 Residency::External {
1456 payload_ref,
1457 digest,
1458 original_size,
1459 } => (
1460 Some(payload_ref.clone()),
1461 Some(digest.clone()),
1462 Some(WireU64::new(*original_size)),
1463 ),
1464 Residency::PagedOut {
1465 payload_ref,
1466 digest,
1467 } => (Some(payload_ref.clone()), Some(digest.clone()), None),
1468 Residency::Resident | Residency::Collapsed => (None, None, None),
1469 };
1470 HandleState {
1471 handle_id: handle.id,
1472 kind: handle_kind_label(&handle.kind).to_string(),
1473 residency: handle.residency.label().to_string(),
1474 payload_ref,
1475 digest,
1476 original_size,
1477 tokens: handle.tokens,
1478 source: handle.source.as_ref().map(|source| source.to_string()),
1479 }
1480 })
1481 .collect(),
1482 next_handle_id: ctx.next_handle_id(),
1483 pending_payload_loads: self
1484 .pending_payload_loads
1485 .iter()
1486 .map(|(effect_id, load)| PendingPayloadLoadState {
1487 effect_id: effect_id.clone(),
1488 handle_id: load.handle_id.clone(),
1489 digest: load.digest.clone(),
1490 original_size: load.original_size.map(WireU64::new),
1491 })
1492 .collect(),
1493 active_skills: ctx
1494 .active_skills
1495 .iter()
1496 .map(|(skill, lease)| SkillLeaseState {
1497 skill: skill.to_string(),
1498 lease_until_turn: *lease,
1499 })
1500 .collect(),
1501 knowledge: ctx
1502 .partitions
1503 .knowledge
1504 .entries
1505 .iter()
1506 .map(|entry| KnowledgeSlotState {
1507 key: entry.key.as_ref().map(|key| key.to_string()),
1508 role: role_label(entry.message.role).to_string(),
1509 body: self.project_body(&entry.message),
1510 tokens: entry.tokens,
1511 pinned: entry.pinned,
1512 evict_at_boundary: entry.evict_at_boundary,
1513 })
1514 .collect(),
1515 signals: ctx.partitions.signals.clone(),
1516 messages: ctx
1517 .partitions
1518 .system
1519 .messages
1520 .iter()
1521 .map(|message| self.project_message(MessagePartition::System, message))
1522 .chain(
1523 ctx.partitions
1524 .history
1525 .messages
1526 .iter()
1527 .map(|message| self.project_message(MessagePartition::History, message)),
1528 )
1529 .collect(),
1530 task_state: project_task_state(&ctx.partitions.task_state),
1531 partition_tokens: PartitionTokenState {
1532 system: ctx.partitions.system.token_count,
1533 knowledge: ctx.partitions.knowledge.token_count,
1534 history: ctx.partitions.history.token_count,
1535 },
1536 history_len: ctx.partitions.history.messages.len() as u32,
1537 frozen_history_len: ctx.frozen_history_len() as u32,
1538 last_activity_ms: WireU64::new(ctx.last_activity_ms),
1539 last_compact_ms: ctx.last_compact_ms.map(WireU64::new),
1540 }
1541 }
1542
1543 fn project_message(
1545 &self,
1546 partition: MessagePartition,
1547 message: &Message,
1548 ) -> StoredMessageState {
1549 StoredMessageState {
1550 partition,
1551 role: role_label(message.role).to_string(),
1552 body: self.project_body(message),
1553 tool_calls: message
1554 .tool_calls
1555 .iter()
1556 .map(|call| LogicalToolCall {
1557 call_id: call.id.to_string(),
1558 name: call.name.to_string(),
1559 arguments: call.arguments.to_string(),
1560 })
1561 .collect(),
1562 tokens: message.token_count.unwrap_or(0),
1563 }
1564 }
1565
1566 fn project_body(&self, message: &Message) -> StoredMessageBody {
1574 let Some((text, tool_call_id, is_error)) = message_body_parts(message) else {
1575 return StoredMessageBody::Structured(StructuredMessageBody {
1576 content_json: serde_json::to_string(&message.content)
1577 .unwrap_or_else(|_| "null".to_string()),
1578 });
1579 };
1580 let Some(call_id) = tool_call_id.as_deref() else {
1581 return StoredMessageBody::Inline(InlineMessageBody {
1582 text,
1583 tool_call_id,
1584 is_error,
1585 });
1586 };
1587 let referenced = self.engine.as_ref().and_then(|engine| {
1588 let handle = engine
1589 .ctx
1590 .handles
1591 .all()
1592 .iter()
1593 .find(|handle| handle.source.as_deref() == Some(call_id))?;
1594 let digest = handle.residency.digest()?;
1595 Some((
1596 handle.id,
1597 digest.to_string(),
1598 matches!(handle.residency, Residency::PagedOut { .. }),
1599 ))
1600 });
1601 match referenced {
1602 Some((handle_id, digest, is_paged_out)) => {
1603 StoredMessageBody::Reference(ReferencedMessageBody {
1604 handle_id,
1605 digest,
1606 preview: if is_paged_out {
1607 crate::context::renderer::collapse_preview(&text, call_id)
1608 } else {
1609 truncate_on_char_boundary(&text, self.preview_bytes())
1610 },
1611 tool_call_id,
1612 is_error,
1613 })
1614 }
1615 None => StoredMessageBody::Inline(InlineMessageBody {
1616 text,
1617 tool_call_id,
1618 is_error,
1619 }),
1620 }
1621 }
1622
1623 fn preview_bytes(&self) -> usize {
1624 self.policy
1625 .as_ref()
1626 .map(|policy| policy.config().payload_policy.preview_bytes as usize)
1627 .unwrap_or(2 * 1024)
1628 }
1629
1630 pub fn restore_logical_state(
1645 genesis_config: &ResolvedOperationConfig,
1646 state: &LogicalKernelState,
1647 ) -> Result<Self, KernelFault> {
1648 let mut driver = Self::new();
1649 let live_config = state
1650 .syscall
1651 .live_config
1652 .clone()
1653 .unwrap_or_else(|| genesis_config.clone());
1654
1655 let mut engine = build_engine(genesis_config);
1659 install_live_policies(&mut engine, &live_config);
1660 engine.set_root_workflow(state.transition.root_kind == Some(RootKind::Workflow));
1661 driver.policy = Some(LivePolicyState::restore(
1662 state.syscall.policy_revision.unwrap_or(WireU64::ZERO),
1663 live_config.clone(),
1664 ));
1665
1666 restore_scheduler(&mut engine, &live_config, &state.scheduler)?;
1667 if let Some(preempt) =
1668 state
1669 .transition
1670 .pending_effects
1671 .iter()
1672 .find_map(|effect| match &effect.effect {
1673 EffectKind::PreemptTasks(preempt) => Some(preempt),
1674 _ => None,
1675 })
1676 {
1677 engine.restore_pending_preempt(
1678 preempt
1679 .attempts
1680 .iter()
1681 .map(|attempt| attempt.task_id.as_str().to_string())
1682 .collect(),
1683 preempt.reason.clone(),
1684 );
1685 }
1686 restore_context_vm(&mut engine, &state.context_vm)?;
1687 engine.restore_memory_write_window(
1688 state
1689 .syscall
1690 .memory_write_window_ms
1691 .iter()
1692 .map(|at| at.get())
1693 .collect(),
1694 );
1695
1696 if let Some(milestone) = &state.scheduler.milestone {
1697 let contract = live_config
1698 .verification_contract(&milestone.contract_id)
1699 .ok_or_else(|| {
1700 KernelFault::new(
1701 KernelFaultCode::CheckpointIncompatible,
1702 format!(
1703 "the checkpoint runs verification contract {:?}, which this \
1704 operation's configuration no longer declares",
1705 milestone.contract_id
1706 ),
1707 )
1708 })?;
1709 engine.load_milestone_contract(core_milestone_contract(contract, &live_config));
1710 if !engine
1711 .restore_milestone_cursor(milestone.phase_id.as_deref(), milestone.blocked_count)
1712 {
1713 return Err(KernelFault::new(
1714 KernelFaultCode::CheckpointIncompatible,
1715 format!(
1716 "the checkpoint sits on milestone phase {:?} of contract {:?}, which that \
1717 contract does not declare",
1718 milestone.phase_id, milestone.contract_id
1719 ),
1720 ));
1721 }
1722 driver.loaded_contract_id = Some(milestone.contract_id.clone());
1723 }
1724
1725 driver.engine = Some(engine);
1726 driver.root_kind = state.transition.root_kind;
1727 driver.focus = state.transition.focus.clone();
1728 if let Some(workflow) = &state.scheduler.workflow {
1729 driver.workflow_id = Some(workflow.workflow_id.clone());
1730 driver.node_ids = workflow
1731 .nodes
1732 .iter()
1733 .map(|node| node.node_id.clone())
1734 .collect();
1735 driver.workflow_nodes = workflow
1736 .nodes
1737 .iter()
1738 .map(|node| WireNode {
1739 node_id: node.node_id.clone(),
1740 task: node.task.clone(),
1741 depends_on: node.depends_on.clone(),
1742 run_spec: node.run_spec.clone(),
1743 })
1744 .collect();
1745 }
1746 driver.attempts = state
1747 .scheduler
1748 .attempts
1749 .iter()
1750 .map(|attempt| {
1751 (
1752 attempt.task_id.as_str().to_string(),
1753 attempt.attempt_id.clone(),
1754 )
1755 })
1756 .collect();
1757 driver.provider_calls = state
1758 .syscall
1759 .provider_calls
1760 .iter()
1761 .map(|call| {
1762 (
1763 call.effect_id.clone(),
1764 PendingProviderCall {
1765 task_id: call.task_id.clone(),
1766 exposed_tools: call.exposed_tools.iter().cloned().collect(),
1767 },
1768 )
1769 })
1770 .collect();
1771 driver.consumed_calls = state.syscall.consumed_call_ids.iter().cloned().collect();
1772 driver.pending_memory_writes = state
1773 .syscall
1774 .authored_memory_writes
1775 .iter()
1776 .map(|write| {
1777 (
1778 write.effect_id.clone(),
1779 AuthoredMemoryWrite {
1780 binding_id: write.binding_id.clone(),
1781 name: write.name.clone(),
1782 kind: write.kind,
1783 size_bytes: write.size_bytes,
1784 },
1785 )
1786 })
1787 .collect();
1788 driver.pending_memory_queries = state
1789 .syscall
1790 .authored_memory_queries
1791 .iter()
1792 .map(|query| {
1793 (
1794 query.effect_id.clone(),
1795 AuthoredMemoryQuery {
1796 binding_id: query.binding_id.clone(),
1797 text: query.text.clone(),
1798 requested_k: query.requested_k,
1799 },
1800 )
1801 })
1802 .collect();
1803 driver.pending_payload_loads = state
1804 .context_vm
1805 .pending_payload_loads
1806 .iter()
1807 .map(|load| {
1808 (
1809 load.effect_id.clone(),
1810 PendingPayloadLoad {
1811 handle_id: load.handle_id.clone(),
1812 digest: load.digest.clone(),
1813 original_size: load.original_size.map(WireU64::get),
1814 },
1815 )
1816 })
1817 .collect();
1818 Ok(driver)
1819 }
1820
1821 pub fn plan(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
1829 if let Some(fault) = &self.poison {
1830 return Err(fault.clone());
1831 }
1832 if let Some(staged) = &self.staged {
1833 let staged_seq = staged.step_seq;
1834 return Err(self.poison_with(KernelFault::new(
1835 KernelFaultCode::TransactionConflict,
1836 format!(
1837 "the driver still holds the plan of step {staged_seq}; its transition never \
1838 committed while the semantic kernel already advanced under it, so this \
1839 runtime no longer describes the journal — rebuild from the records"
1840 ),
1841 )));
1842 }
1843 let mut step = self.plan_inner(context)?;
1844 step.observations = self
1845 .engine
1846 .as_mut()
1847 .map(LoopStateMachine::take_observations)
1848 .unwrap_or_default();
1849 self.staged = Some(StagedFocus {
1850 step_seq: context.step_seq,
1851 root_kind: step.root_kind,
1852 focus: step.focus.clone(),
1853 });
1854 Ok(step)
1855 }
1856
1857 pub fn note_committed(&mut self, step_seq: WireU64) -> Result<(), KernelFault> {
1860 if let Some(fault) = &self.poison {
1861 return Err(fault.clone());
1862 }
1863 let Some(staged) = self.staged.take() else {
1864 return Err(self.poison_with(KernelFault::new(
1865 KernelFaultCode::TransactionConflict,
1866 format!("step {step_seq} committed, but the driver planned no such step"),
1867 )));
1868 };
1869 if staged.step_seq != step_seq {
1870 let planned = staged.step_seq;
1871 return Err(self.poison_with(KernelFault::new(
1872 KernelFaultCode::TransactionConflict,
1873 format!("step {step_seq} committed, but the driver planned step {planned}"),
1874 )));
1875 }
1876 if let Some(kind) = staged.root_kind {
1877 self.root_kind = Some(kind);
1878 }
1879 self.focus = staged.focus;
1880 Ok(())
1881 }
1882
1883 pub fn fold(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
1887 let step = self.plan(context)?;
1888 self.note_committed(context.step_seq)?;
1889 Ok(step)
1890 }
1891
1892 pub fn begin_nested_workflow(
1905 &mut self,
1906 context: &PlanContext<'_>,
1907 spec: &WireSpec,
1908 ) -> Result<PlannedStep, KernelFault> {
1909 if let Some(fault) = &self.poison {
1910 return Err(fault.clone());
1911 }
1912 let mut index = 0;
1913 let outcome = self
1914 .enter_nested_workflow(context, spec, &mut index)
1915 .map_err(|refusal| match refusal {
1916 SyscallRefusal::Fault(fault) => fault,
1917 SyscallRefusal::Rejected(rejected) => {
1920 KernelFault::new(KernelFaultCode::ResourceLimitExceeded, rejected.reason)
1921 }
1922 })?;
1923 let step = PlannedStep {
1924 root_kind: Some(RootKind::Agent),
1925 focus: outcome.focus,
1926 observations: self
1927 .engine
1928 .as_mut()
1929 .map(LoopStateMachine::take_observations)
1930 .unwrap_or_default(),
1931 disposition: StepDisposition::Effects(EffectsDisposition {
1932 effects: outcome.effects,
1933 }),
1934 };
1935 self.staged = Some(StagedFocus {
1936 step_seq: context.step_seq,
1937 root_kind: step.root_kind,
1938 focus: step.focus.clone(),
1939 });
1940 Ok(step)
1941 }
1942
1943 fn enter_nested_workflow(
1946 &mut self,
1947 context: &PlanContext<'_>,
1948 spec: &WireSpec,
1949 effect_index: &mut u32,
1950 ) -> Result<SyscallOutcome, SyscallRefusal> {
1951 let staged = self.staged.as_ref().map(|staged| staged.focus.clone());
1952 let focus = staged.as_ref().unwrap_or(&self.focus);
1953 let root_kind = self.root_kind;
1954
1955 let parent_task_id = match (root_kind, focus) {
1956 (Some(RootKind::Agent), Some(ExecutionFocus::AgentTurn(turn))) => turn.task_id.clone(),
1957 (Some(RootKind::Agent), Some(ExecutionFocus::WorkflowController(_))) => {
1958 return Err(authority(
1959 "a workflow is already the execution focus; workflows do not stack, so a \
1960 second start request is refused with no spawn effect (§7.4 focus depth ≤ 1)",
1961 ));
1962 }
1963 (Some(RootKind::Workflow), _) => {
1964 return Err(authority(
1965 "this operation's root is a workflow; its focus never moves, and a nested \
1966 workflow start is not a transition it admits (§7.4)",
1967 ));
1968 }
1969 _ => {
1970 return Err(SyscallRefusal::Fault(KernelFault::new(
1971 KernelFaultCode::InvalidLifecycle,
1972 "no root has started, so there is no agent turn to suspend".to_string(),
1973 )));
1974 }
1975 };
1976
1977 for node in &spec.nodes {
1978 self.require_known_contract(context.config, node.run_spec.as_ref())
1979 .map_err(|fault| {
1980 SyscallRefusal::Rejected(SyscallRejection::new("start_workflow", fault.message))
1981 })?;
1982 }
1983 let core_spec = build_core_spec(spec).map_err(SyscallRefusal::Fault)?;
1984 let node_ids = wire_node_ids(spec);
1985 let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
1986 self.require_effect_support(context.config, EffectKindTag::SpawnTasks)
1987 .map_err(SyscallRefusal::Fault)?;
1988
1989 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
1992 let disposition = engine.gate_syscall(&CoreSyscall::LoadWorkflow {
1993 node_count: spec.nodes.len(),
1994 });
1995 if !disposition.is_allowed() {
1996 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
1997 "start_workflow",
1998 denial_reason(&disposition, "workflow authoring denied"),
1999 )));
2000 }
2001
2002 engine.set_root_workflow(false);
2004 let action = engine.load_workflow(core_spec);
2005 self.node_ids = node_ids;
2006 self.workflow_nodes = spec.nodes.clone();
2007 self.workflow_id = Some(workflow_id.clone());
2008 let disposition = self
2009 .disposition_for_at(context, action, RootKind::Agent, effect_index)
2010 .map_err(SyscallRefusal::Fault)?;
2011 let StepDisposition::Effects(effects) = disposition else {
2012 return Err(SyscallRefusal::Fault(KernelFault::new(
2013 KernelFaultCode::InvalidLifecycle,
2014 "entering a nested workflow cannot terminate the operation".to_string(),
2015 )));
2016 };
2017 Ok(SyscallOutcome {
2018 effects: effects.effects,
2019 focus: Some(ExecutionFocus::workflow_controller(
2020 workflow_id,
2021 Some(parent_task_id),
2022 )),
2023 needs_workflow_round: false,
2024 })
2025 }
2026
2027 fn derive_provider_syscalls(
2042 &self,
2043 effect_id: &EffectId,
2044 calls: &[WireToolCall],
2045 ) -> Result<Vec<(SyscallCausation, WireToolCall)>, KernelFault> {
2046 let Some(pending) = self.provider_calls.get(effect_id) else {
2047 return Err(KernelFault::new(
2048 KernelFaultCode::InvalidAuthority,
2049 format!(
2050 "effect {effect_id} is not a provider call this kernel published, so a tool \
2051 call inside its result has no caller to derive (§7.6)"
2052 ),
2053 ));
2054 };
2055 let mut seen: BTreeSet<&str> = BTreeSet::new();
2056 let mut derived = Vec::with_capacity(calls.len());
2057 for call in calls {
2058 if !pending.exposed_tools.contains(&call.name) {
2059 return Err(KernelFault::new(
2060 KernelFaultCode::InvalidAuthority,
2061 format!(
2062 "the turn behind effect {effect_id} exposed no tool named {:?}; a caller is \
2063 derived from the surface the kernel published, never from the name a \
2064 result carries (§7.6)",
2065 call.name
2066 ),
2067 ));
2068 }
2069 if self.consumed_calls.contains(call.call_id.as_str())
2070 || !seen.insert(call.call_id.as_str())
2071 {
2072 return Err(KernelFault::new(
2073 KernelFaultCode::InvalidAuthority,
2074 format!(
2075 "call {} already produced a syscall; a causation is consumed once (§7.6)",
2076 call.call_id
2077 ),
2078 ));
2079 }
2080 derived.push((
2081 SyscallCausation::ProviderTool(ProviderToolCausation {
2082 provider_effect_id: effect_id.clone(),
2083 call_id: call.call_id.clone(),
2084 task_id: pending.task_id.clone(),
2085 }),
2086 call.clone(),
2087 ));
2088 }
2089 Ok(derived)
2090 }
2091
2092 fn plan_provider_completed(
2108 &mut self,
2109 context: &PlanContext<'_>,
2110 effect_id: &EffectId,
2111 completed: &ProviderCompleted,
2112 ) -> Result<PlannedStep, KernelFault> {
2113 let root_kind = self.require_root_kind()?;
2114 self.require_pending_provider_call(effect_id)?;
2115 let syscalls: Vec<WireToolCall> = completed
2116 .message
2117 .tool_calls
2118 .iter()
2119 .filter(|call| is_syscall_tool(&call.name))
2120 .cloned()
2121 .collect();
2122 let derived = self.derive_provider_syscalls(effect_id, &syscalls)?;
2123 let message = core_provider_message(&completed.message)?;
2124
2125 let engine = self.engine_mut()?;
2127 if let Some(tokens) = completed.observed_input_tokens {
2128 engine.ctx.set_observed_prompt_tokens(tokens);
2129 }
2130 engine.set_output_truncated(matches!(
2133 completed.stop_reason,
2134 Some(super::effect::ProviderStopReason::MaxTokens)
2135 ));
2136
2137 let mut index = 0u32;
2138 let mut effects = Vec::new();
2139 let mut syscall_focus: Option<ExecutionFocus> = None;
2140 let mut answered: Vec<AnsweredCall> = Vec::with_capacity(derived.len());
2141 let mut needs_round = false;
2142 for (causation, call) in &derived {
2143 let outcome = match decode_syscall(call) {
2144 Ok(request) => self.apply_syscall(context, causation, &request, &mut index),
2145 Err(rejection) => Err(SyscallRefusal::Rejected(rejection)),
2146 };
2147 match outcome {
2148 Ok(outcome) => {
2149 answered.push(AnsweredCall {
2150 call_id: call.call_id.as_str().into(),
2151 output: syscall_ack(&call.name).to_string(),
2152 is_error: false,
2153 });
2154 effects.extend(outcome.effects);
2155 if let Some(next) = outcome.focus {
2156 syscall_focus = Some(next);
2157 }
2158 needs_round |= outcome.needs_workflow_round;
2159 }
2160 Err(SyscallRefusal::Fault(fault)) => return Err(fault),
2161 Err(SyscallRefusal::Rejected(rejection)) => {
2162 answered.push(AnsweredCall {
2163 call_id: call.call_id.as_str().into(),
2164 output: rejection.reason.clone(),
2165 is_error: true,
2166 });
2167 self.note_rejection(rejection.by(&causation_task(causation)))
2168 }
2169 }
2170 }
2171 let mut awaits_kernel_work = !effects.is_empty();
2174 if needs_round {
2175 awaits_kernel_work = true;
2176 let action = self.engine_mut()?.drive_workflow_round();
2177 self.extend_with_action(context, action, root_kind, &mut index, &mut effects)?;
2178 }
2179
2180 let engine = self.engine_mut()?;
2181 engine.stage_adjudicated_turn(AdjudicatedTurn {
2182 answered_calls: answered,
2183 idle_continuation: if awaits_kernel_work {
2184 IdleContinuation::Await
2185 } else {
2186 IdleContinuation::CallProvider
2187 },
2188 });
2189 let syscall_observations = engine.take_observations();
2192 let action = engine.feed(LoopEvent::LLMResponse { message });
2193 let mut step = self.continue_after_at(context, action, root_kind, &mut index)?;
2194 if let Some(engine) = self.engine.as_mut() {
2195 engine.observations.splice(0..0, syscall_observations);
2196 }
2197 if syscall_focus.is_some() {
2198 step.focus = syscall_focus;
2199 }
2200 if !effects.is_empty() {
2201 match &mut step.disposition {
2202 StepDisposition::Effects(published) => {
2203 let mut merged = effects;
2204 merged.append(&mut published.effects);
2205 published.effects = merged;
2206 }
2207 StepDisposition::Terminal(_) => {
2208 return Err(KernelFault::new(
2209 KernelFaultCode::InvalidLifecycle,
2210 "a provider turn that terminates the operation cannot also publish the \
2211 effects its syscalls asked for (§7.12)"
2212 .to_string(),
2213 ));
2214 }
2215 }
2216 }
2217
2218 self.provider_calls.remove(effect_id);
2223 for (_, call) in &derived {
2224 self.consumed_calls
2225 .insert(call.call_id.as_str().to_string());
2226 }
2227 Ok(step)
2228 }
2229
2230 fn apply_syscall(
2233 &mut self,
2234 context: &PlanContext<'_>,
2235 causation: &SyscallCausation,
2236 request: &SyscallRequest,
2237 effect_index: &mut u32,
2238 ) -> Result<SyscallOutcome, SyscallRefusal> {
2239 let caller = causation_task(causation);
2240 let quarantined = self
2241 .engine
2242 .as_ref()
2243 .is_some_and(|engine| engine.task_quarantined(caller.as_str()));
2244 if quarantined && let Some(family) = privileged_family(request) {
2245 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2250 family,
2251 format!(
2252 "quarantine: task {caller} is quarantined and may not widen its authority \
2253 through a {family} syscall"
2254 ),
2255 )));
2256 }
2257
2258 match request {
2259 SyscallRequest::SubmitWorkflow(submit) => {
2260 if submit.spec.nodes.is_empty() {
2261 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2262 "start_workflow",
2263 "an authored workflow with no nodes has nothing to spawn",
2264 )));
2265 }
2266 if self
2267 .engine
2268 .as_ref()
2269 .is_some_and(LoopStateMachine::workflow_active)
2270 {
2271 self.append_nodes(
2274 context.config,
2275 &submit.spec.nodes,
2276 &caller,
2277 CoreSyscall::LoadWorkflow { node_count: 0 },
2278 "start_workflow",
2279 )
2280 } else {
2281 self.enter_nested_workflow(context, &submit.spec, effect_index)
2282 }
2283 }
2284 SyscallRequest::AppendWorkflowNodes(append) => {
2285 if append.nodes.is_empty() {
2286 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2287 "submit_workflow_nodes",
2288 "an empty submission appends nothing",
2289 )));
2290 }
2291 if !self
2292 .engine
2293 .as_ref()
2294 .is_some_and(LoopStateMachine::workflow_active)
2295 {
2296 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2297 "submit_workflow_nodes",
2298 "no workflow is in flight, so there is no graph to append to",
2299 )));
2300 }
2301 self.append_nodes(
2302 context.config,
2303 &append.nodes,
2304 &caller,
2305 CoreSyscall::SubmitNodes { count: 0 },
2306 "submit_workflow_nodes",
2307 )
2308 }
2309 SyscallRequest::ActivateSkill(activate) => {
2310 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2311 if !engine.ctx.skill_available(&activate.name) {
2312 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2313 "skill",
2314 format!(
2315 "this operation declares no skill named {:?}; activation is a \
2316 capability mutation and is refused rather than invented",
2317 activate.name
2318 ),
2319 )));
2320 }
2321 let expires_at_turn = activate
2322 .lease_turns
2323 .map(|turns| engine.turn.saturating_add(turns));
2324 engine
2325 .ctx
2326 .activate_skill_leased(activate.name.as_str(), expires_at_turn);
2327 Ok(SyscallOutcome::default())
2328 }
2329 SyscallRequest::UpdateTask(update) => {
2330 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2331 engine.ctx.update_task(core_task_update(&update.update));
2332 Ok(SyscallOutcome::default())
2333 }
2334 SyscallRequest::RequestMemoryWrite(write) => {
2335 self.plan_memory_write(context, causation, &write.proposal, effect_index)
2336 }
2337 SyscallRequest::RequestMemoryQuery(query) => {
2338 self.plan_memory_query(context, causation, &query.query, effect_index)
2339 }
2340 SyscallRequest::PageIn(page_in) => {
2341 self.plan_page_in(context, &page_in.handle_id, effect_index)
2342 }
2343 }
2344 }
2345
2346 fn append_nodes(
2348 &mut self,
2349 config: &ResolvedOperationConfig,
2350 nodes: &[WireNode],
2351 caller: &TaskId,
2352 syscall: CoreSyscall,
2353 label: &'static str,
2354 ) -> Result<SyscallOutcome, SyscallRefusal> {
2355 for node in nodes {
2359 self.require_known_contract(config, node.run_spec.as_ref())
2360 .map_err(|fault| {
2361 SyscallRefusal::Rejected(SyscallRejection::new(label, fault.message))
2362 })?;
2363 }
2364 let core_nodes = build_core_spec(&WireSpec {
2367 name: String::new(),
2368 nodes: nodes.to_vec(),
2369 })
2370 .map_err(|fault| SyscallRefusal::Rejected(SyscallRejection::new(label, fault.message)))?
2371 .nodes;
2372
2373 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2374 let admitted = engine.append_workflow_nodes(
2375 core_nodes,
2376 Some(caller.as_str()),
2380 syscall,
2383 label,
2384 );
2385 if !admitted {
2386 return Ok(SyscallOutcome::default());
2389 }
2390 self.node_ids
2395 .extend(nodes.iter().map(|node| node.node_id.clone()));
2396 self.workflow_nodes.extend_from_slice(nodes);
2397 Ok(SyscallOutcome {
2398 effects: Vec::new(),
2399 focus: None,
2400 needs_workflow_round: true,
2401 })
2402 }
2403
2404 fn plan_memory_write(
2405 &mut self,
2406 context: &PlanContext<'_>,
2407 causation: &SyscallCausation,
2408 proposal: &super::syscall::MemoryWriteProposal,
2409 effect_index: &mut u32,
2410 ) -> Result<SyscallOutcome, SyscallRefusal> {
2411 let Some(binding) = context.config.memory_access.clone() else {
2412 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2413 "write_memory",
2414 "this operation holds no memory binding, so it can author no memory record",
2415 )));
2416 };
2417 if !binding.capabilities.write {
2418 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2419 "write_memory",
2420 "this operation's memory binding is read-only",
2421 )));
2422 }
2423 self.require_effect_support(context.config, EffectKindTag::PersistMemory)
2424 .map_err(SyscallRefusal::Fault)?;
2425 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2426 let disposition = engine.gate_memory_write_proposal();
2427 if !disposition.is_allowed() {
2428 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2429 "write_memory",
2430 denial_reason(&disposition, "memory write denied"),
2431 )));
2432 }
2433 let authored = AuthoredMemoryWrite {
2437 binding_id: binding.binding_id.clone(),
2438 name: proposal.name.clone(),
2439 kind: proposal.kind,
2440 size_bytes: proposal.content.len() as u32,
2441 };
2442 let effect = EffectKind::PersistMemory(PersistMemoryEffect {
2443 binding,
2444 memory: CanonicalMemoryWrite {
2445 name: proposal.name.clone(),
2446 kind: proposal.kind,
2447 content: proposal.content.clone(),
2448 description: proposal.description.clone(),
2449 evidence_refs: proposal.evidence_refs.clone(),
2450 accepted_at_ms: context.input.observed_at_ms,
2451 causation: causation.clone(),
2452 },
2453 });
2454 let published = self.mint_effect(context, effect, effect_index);
2455 self.pending_memory_writes
2456 .insert(published.effect_id.clone(), authored);
2457 Ok(SyscallOutcome {
2458 effects: vec![published],
2459 focus: None,
2460 needs_workflow_round: false,
2461 })
2462 }
2463
2464 fn plan_memory_query(
2465 &mut self,
2466 context: &PlanContext<'_>,
2467 causation: &SyscallCausation,
2468 proposal: &super::syscall::MemoryQueryProposal,
2469 effect_index: &mut u32,
2470 ) -> Result<SyscallOutcome, SyscallRefusal> {
2471 let Some(binding) = context.config.memory_access.clone() else {
2472 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2473 "query_memory",
2474 "this operation holds no memory binding, so it can read no memory record",
2475 )));
2476 };
2477 if !binding.capabilities.read {
2478 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2479 "query_memory",
2480 "this operation's memory binding is write-only",
2481 )));
2482 }
2483 self.require_effect_support(context.config, EffectKindTag::QueryMemory)
2484 .map_err(SyscallRefusal::Fault)?;
2485 let ceiling = context.config.memory_policy.retrieval_top_k;
2488 let requested_k = proposal.limit.unwrap_or(ceiling).clamp(1, ceiling);
2489 let authored = AuthoredMemoryQuery {
2490 binding_id: binding.binding_id.clone(),
2491 text: proposal.text.clone(),
2492 requested_k,
2493 };
2494 let effect = EffectKind::QueryMemory(QueryMemoryEffect {
2495 binding,
2496 query: CanonicalMemoryQuery {
2497 text: proposal.text.clone(),
2498 kinds: proposal.kinds.clone(),
2499 accepted_at_ms: context.input.observed_at_ms,
2500 causation: causation.clone(),
2501 },
2502 requested_k,
2503 });
2504 let published = self.mint_effect(context, effect, effect_index);
2505 self.pending_memory_queries
2506 .insert(published.effect_id.clone(), authored);
2507 Ok(SyscallOutcome {
2508 effects: vec![published],
2509 focus: None,
2510 needs_workflow_round: false,
2511 })
2512 }
2513
2514 fn plan_page_in(
2529 &mut self,
2530 context: &PlanContext<'_>,
2531 handle_id: &super::scalar::HandleId,
2532 effect_index: &mut u32,
2533 ) -> Result<SyscallOutcome, SyscallRefusal> {
2534 let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2535 let Some(residency) = engine.ctx.payload_residency(handle_id.as_str()).cloned() else {
2536 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2537 READ_RESULT_TOOL_NAME,
2538 format!(
2539 "handle {handle_id} is not reachable in this operation's handle table; a \
2540 page-in addresses only what the caller already holds"
2541 ),
2542 )));
2543 };
2544 let (Some(payload_ref), Some(digest)) = (residency.payload_ref(), residency.digest())
2545 else {
2546 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2547 READ_RESULT_TOOL_NAME,
2548 format!(
2549 "handle {handle_id} is {} — its body is held by this kernel, not by the \
2550 payload store, so there is nothing to page in",
2551 residency.label()
2552 ),
2553 )));
2554 };
2555 let payload_ref = PayloadRef::new(payload_ref).map_err(|error| {
2556 SyscallRefusal::Fault(KernelFault::new(
2557 KernelFaultCode::MalformedEnvelope,
2558 format!(
2559 "handle {handle_id} records an unusable payload locator: {}",
2560 error.message
2561 ),
2562 ))
2563 })?;
2564 self.require_effect_support(context.config, EffectKindTag::LoadPayload)
2565 .map_err(SyscallRefusal::Fault)?;
2566 let effect = EffectKind::LoadPayload(LoadPayloadEffect {
2567 handle_id: handle_id.clone(),
2568 payload_ref,
2569 });
2570 let published = self.mint_effect(context, effect, effect_index);
2571 self.pending_payload_loads.insert(
2572 published.effect_id.clone(),
2573 PendingPayloadLoad {
2574 handle_id: handle_id.as_str().to_string(),
2575 digest: digest.to_string(),
2576 original_size: match &residency {
2577 Residency::External { original_size, .. } => Some(*original_size),
2578 _ => None,
2579 },
2580 },
2581 );
2582 Ok(SyscallOutcome {
2583 effects: vec![published],
2584 focus: None,
2585 needs_workflow_round: false,
2586 })
2587 }
2588
2589 fn note_rejection(&mut self, rejection: SyscallRejection) {
2591 let Some(engine) = self.engine.as_mut() else {
2592 return;
2593 };
2594 let note = crate::scheduler::rollback::build_control_rejection_note(
2595 rejection.operation,
2596 &rejection.reason,
2597 engine.ctx.config.verbose_control_notes,
2598 );
2599 engine.ctx.push_signal(note);
2600 let turn = engine.turn;
2601 engine
2602 .observations
2603 .push(KernelObservation::ControlRequestRejected {
2604 turn,
2605 operation: rejection.operation.to_string(),
2606 subject: rejection.subject,
2607 reason: rejection.reason,
2608 });
2609 }
2610
2611 fn mint_effect(
2612 &self,
2613 context: &PlanContext<'_>,
2614 effect: EffectKind,
2615 effect_index: &mut u32,
2616 ) -> KernelEffect {
2617 let effect_id =
2618 mint_effect_id(&context.input.operation_id, context.step_seq, *effect_index);
2619 *effect_index += 1;
2620 KernelEffect {
2621 effect_id,
2622 causation_input_id: context.input.input_id.clone(),
2623 effect,
2624 }
2625 }
2626
2627 fn extend_with_action(
2629 &mut self,
2630 context: &PlanContext<'_>,
2631 action: LoopAction,
2632 root_kind: RootKind,
2633 effect_index: &mut u32,
2634 effects: &mut Vec<KernelEffect>,
2635 ) -> Result<(), KernelFault> {
2636 match self.disposition_for_at(context, action, root_kind, effect_index)? {
2637 StepDisposition::Effects(published) => {
2638 effects.extend(published.effects);
2639 Ok(())
2640 }
2641 StepDisposition::Terminal(_) => Err(KernelFault::new(
2642 KernelFaultCode::InvalidLifecycle,
2643 "a syscall batch cannot terminate the operation; §7.12 admits effects or a \
2644 terminal, never both in one step"
2645 .to_string(),
2646 )),
2647 }
2648 }
2649
2650 fn plan_inner(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
2653 if let Some(engine) = self.engine.as_mut() {
2658 engine.take_observations();
2659 engine.observe_accepted_time(context.input.observed_at_ms.get());
2664 }
2665 match &context.input.input {
2666 NormalizedPayload::ConfigureOperation(configure) => {
2667 self.plan_configure(&configure.config)
2668 }
2669 NormalizedPayload::StartOperation(start) => {
2670 self.plan_start(context, &start.entry, &start.initial_context)
2671 }
2672 NormalizedPayload::ResolveEffect(resolve) => self.plan_resolve_effect(context, resolve),
2673 NormalizedPayload::DeliverExternalEvent(event) => {
2674 self.plan_external_event(context, &event.event)
2675 }
2676 NormalizedPayload::HostControl(control) => {
2677 self.plan_host_control(context, &control.command)
2678 }
2679 }
2680 }
2681
2682 fn plan_configure(
2685 &mut self,
2686 config: &ResolvedOperationConfig,
2687 ) -> Result<PlannedStep, KernelFault> {
2688 self.engine = Some(build_engine(config));
2689 self.policy = Some(LivePolicyState::new(config.clone()));
2692 Ok(PlannedStep::quiet(None, None))
2693 }
2694
2695 fn plan_start(
2702 &mut self,
2703 context: &PlanContext<'_>,
2704 entry: &RootEntry,
2705 initial: &InitialContext,
2706 ) -> Result<PlannedStep, KernelFault> {
2707 if self.root_kind.is_some() || self.staged.is_some() {
2708 return Err(KernelFault::new(
2709 KernelFaultCode::InvalidLifecycle,
2710 "this operation already has a root; a root entry is chosen once and is immutable \
2711 (§6.1.3–6.1.5)"
2712 .to_string(),
2713 ));
2714 }
2715
2716 match entry {
2717 RootEntry::Agent(agent) => {
2718 self.require_effect_support(context.config, EffectKindTag::CallProvider)?;
2719 self.require_known_contract(context.config, agent.run_spec.as_ref())?;
2720 let task = runtime_task(&agent.task);
2721 let run_spec = agent.run_spec.as_ref().map(agent_run_spec);
2722
2723 self.load_verification_contract(context.config, agent.run_spec.as_ref())?;
2727 let engine = self.engine_mut()?;
2728 seed_initial_context(engine, initial);
2729 engine.run_spec = run_spec;
2730 let action = engine.start(task);
2731 let disposition = self.disposition_for(context, action, RootKind::Agent)?;
2732 if !publishes(&disposition, EffectKindTag::CallProvider) {
2733 return Err(KernelFault::new(
2734 KernelFaultCode::InvalidLifecycle,
2735 "an agent root's first committed step must publish a provider call (§7.4)"
2736 .to_string(),
2737 ));
2738 }
2739 Ok(PlannedStep {
2740 root_kind: Some(RootKind::Agent),
2741 focus: Some(ExecutionFocus::agent_turn(root_task_id())),
2742 observations: Vec::new(),
2743 disposition,
2744 })
2745 }
2746 RootEntry::Workflow(workflow) => {
2747 self.require_effect_support(context.config, EffectKindTag::SpawnTasks)?;
2748 for node in &workflow.spec.nodes {
2749 self.require_known_contract(context.config, node.run_spec.as_ref())?;
2750 }
2751 if workflow.spec.nodes.is_empty() {
2752 return Err(KernelFault::new(
2753 KernelFaultCode::InvalidConfig,
2754 "a workflow root with no nodes has no first task to spawn; a root entry \
2755 must be able to publish its first effect (§10.1)"
2756 .to_string(),
2757 ));
2758 }
2759 let core_spec = build_core_spec(&workflow.spec)?;
2760 let node_ids = wire_node_ids(&workflow.spec);
2761 let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
2762
2763 let engine = self.engine_mut()?;
2765 seed_initial_context(engine, initial);
2766 engine.set_root_workflow(true);
2769 let action = engine.load_workflow(core_spec);
2770 self.node_ids = node_ids;
2771 self.workflow_nodes = workflow.spec.nodes.clone();
2772 self.workflow_id = Some(workflow_id.clone());
2773 let disposition = self.disposition_for(context, action, RootKind::Workflow)?;
2774 if !publishes(&disposition, EffectKindTag::SpawnTasks) {
2775 return Err(KernelFault::new(
2776 KernelFaultCode::InvalidLifecycle,
2777 "a workflow root's first committed step must publish a task spawn, never a \
2778 provider call (§10.1)"
2779 .to_string(),
2780 ));
2781 }
2782 Ok(PlannedStep {
2783 root_kind: Some(RootKind::Workflow),
2784 focus: Some(ExecutionFocus::workflow_controller(workflow_id, None)),
2785 observations: Vec::new(),
2786 disposition,
2787 })
2788 }
2789 }
2790 }
2791
2792 fn plan_resolve_effect(
2798 &mut self,
2799 context: &PlanContext<'_>,
2800 resolve: &ResolveEffect,
2801 ) -> Result<PlannedStep, KernelFault> {
2802 match &resolve.outcome {
2803 EffectOutcome::Succeeded(success) => {
2804 self.plan_effect_success(context, &resolve.effect_id, &success.result)
2805 }
2806 EffectOutcome::Failed(failed) => {
2807 self.plan_effect_failure(context, &resolve.effect_id, &failed.failure)
2808 }
2809 }
2810 }
2811
2812 fn plan_effect_success(
2814 &mut self,
2815 context: &PlanContext<'_>,
2816 effect_id: &EffectId,
2817 success: &EffectSuccess,
2818 ) -> Result<PlannedStep, KernelFault> {
2819 match success {
2820 EffectSuccess::Provider(provider) => match &provider.outcome {
2821 ProviderOutcome::Completed(completed) => {
2822 self.plan_provider_completed(context, effect_id, completed)
2823 }
2824 ProviderOutcome::ContextOverflow(overflow) => {
2827 let root_kind = self.require_root_kind()?;
2828 self.require_pending_provider_call(effect_id)?;
2829 let engine = self.engine_mut()?;
2830 if let Some(tokens) = overflow.observed_input_tokens {
2831 engine.ctx.set_observed_prompt_tokens(tokens);
2832 }
2833 let action = engine.recover_from_context_overflow();
2834 self.provider_calls.remove(effect_id);
2835 self.continue_after(context, action, root_kind)
2836 }
2837 },
2838 EffectSuccess::Tools(tools) => {
2839 let root_kind = self.require_root_kind()?;
2840 for payload in &tools.results {
2844 check_payload_policy(payload, &context.config.payload_policy)?;
2845 }
2846 let mut results: Vec<ToolResult> =
2847 tools.results.iter().map(core_tool_result).collect();
2848 results.extend(self.close_out_fatal_batch(&tools.results)?);
2849 let mut action = self.engine_mut()?.feed(LoopEvent::ToolResults { results });
2850 self.record_external_payloads(&tools.results)?;
2851 self.engine_mut()?.refresh_call_llm_action(&mut action);
2852 self.continue_after(context, action, root_kind)
2853 }
2854 EffectSuccess::Approval(approval) => {
2855 let root_kind = self.require_root_kind()?;
2856 let approved = approval
2857 .approved_call_ids
2858 .iter()
2859 .map(|id| id.as_str().to_string())
2860 .collect();
2861 let denied = approval
2862 .denied_call_ids
2863 .iter()
2864 .map(|id| id.as_str().to_string())
2865 .collect();
2866 let action = self.engine_mut()?.resolve_approval(approved, denied);
2867 self.continue_after(context, action, root_kind)
2868 }
2869 EffectSuccess::TasksSpawned(spawned) => {
2870 let mut started: Vec<String> = Vec::new();
2871 let mut failures: Vec<WorkflowSpawnFailure> = Vec::new();
2872 for attempt in &spawned.attempts {
2873 let agent_id = attempt.task_id.as_str().to_string();
2874 match &attempt.outcome {
2875 super::effect::TaskLaunchStatus::Started(_) => started.push(agent_id),
2876 super::effect::TaskLaunchStatus::Failed(failed) => {
2877 self.attempts.remove(&agent_id);
2881 failures.push(WorkflowSpawnFailure {
2882 agent_id,
2883 error: failed.failure.message.clone(),
2884 });
2885 }
2886 }
2887 }
2888 let root_kind = self.require_root_kind()?;
2889 let action = self.engine_mut()?.resolve_workflow_spawn(started, failures);
2890 self.continue_after(context, action, root_kind)
2891 }
2892 EffectSuccess::TasksPreempted(preempted) => {
2893 let root_kind = self.require_root_kind()?;
2894 for attempt in &preempted.attempts {
2898 self.attempts.remove(attempt.task_id.as_str());
2899 }
2900 let action = self.engine_mut()?.resolve_preempt();
2901 self.continue_after(context, action, root_kind)
2902 }
2903 EffectSuccess::MemoryPersisted(persisted) => {
2904 self.commit_memory_write(effect_id, Some(&persisted.receipt), None)
2905 }
2906 EffectSuccess::MemoryQueried(queried) => {
2907 let root_kind = self.require_root_kind()?;
2908 let Some(query) = self.pending_memory_queries.remove(effect_id) else {
2909 return Err(unowned_resolution(effect_id, "memory query"));
2910 };
2911 let turn = self.engine_mut()?.turn;
2912 let mut recalled = Vec::with_capacity(queried.recalls.len());
2913 for recall in &queried.recalls {
2914 let content = format!(
2918 "[MEMORY record_ref={} kind={}] {}",
2919 recall.record_ref,
2920 wire_memory_kind_label(recall.kind),
2921 recall.content
2922 );
2923 let engine = self.engine_mut()?;
2924 let tokens = engine.ctx.engine.count(&content).max(1);
2925 engine.ctx.push_history(Message::user(content), tokens);
2926 recalled.push(recall.record_ref.as_str().to_string());
2927 }
2928 let engine = self.engine_mut()?;
2933 let action = engine.resume_after_preload();
2934 engine.observations.push(KernelObservation::MemoryQueried {
2935 turn,
2936 scope: binding_scope(&query.binding_id),
2937 query: query.text.clone(),
2938 requested_k: query.requested_k as usize,
2939 requires_async_response: false,
2940 });
2941 let mut step = self.continue_after(context, action, root_kind)?;
2942 step.focus = self.focus.clone();
2943 Ok(step)
2944 }
2945 EffectSuccess::PageOutArchived(archived) => {
2946 let root_kind = self.require_root_kind()?;
2947 self.verify_page_out_receipt(context, effect_id, &archived.receipt)?;
2948 let receipt = &archived.receipt;
2949 let action = self
2950 .engine_mut()?
2951 .commit_page_out_archive(Some(receipt.payload_ref.as_str().to_string()));
2952 let engine = self.engine_mut()?;
2956 let turn = engine.turn;
2957 let previous = engine.ctx.set_payload_residency(
2958 receipt.handle_id.as_str(),
2959 HandleKind::MemoryPage,
2960 0,
2961 Residency::PagedOut {
2962 payload_ref: receipt.payload_ref.as_str().to_string(),
2963 digest: receipt.digest.as_str().to_string(),
2964 },
2965 );
2966 engine
2967 .observations
2968 .push(KernelObservation::PayloadResidencyChanged {
2969 turn,
2970 handle_id: receipt.handle_id.as_str().to_string(),
2971 from: previous.map(|residency| residency.label().to_string()),
2972 to: "paged_out".to_string(),
2973 payload_ref: Some(receipt.payload_ref.as_str().to_string()),
2974 original_size: receipt.original_size.get(),
2975 });
2976 self.continue_after(context, action, root_kind)
2977 }
2978 EffectSuccess::MilestoneEvaluated(evaluated) => {
2979 let root_kind = self.require_root_kind()?;
2980 let action = self.engine_mut()?.feed(LoopEvent::MilestoneResult {
2981 result: core_milestone_result(&evaluated.result),
2982 });
2983 self.continue_after(context, action, root_kind)
2984 }
2985 EffectSuccess::PayloadLoaded(loaded) => {
2986 self.commit_payload_load(context, effect_id, loaded)
2987 }
2988 }
2989 }
2990
2991 fn close_out_fatal_batch(
3011 &mut self,
3012 submitted: &[WireToolResultPayload],
3013 ) -> Result<Vec<ToolResult>, KernelFault> {
3014 let fatal = submitted
3015 .iter()
3016 .any(|payload| payload.disposition().is_fatal());
3017 if !fatal {
3018 return Ok(Vec::new());
3019 }
3020 let answered: BTreeSet<&str> = submitted
3021 .iter()
3022 .map(|payload| payload.call_id().as_str())
3023 .collect();
3024 Ok(self
3025 .engine_mut()?
3026 .dispatched_tool_calls()
3027 .iter()
3028 .filter(|call| !answered.contains(call.id.as_str()))
3029 .map(|call| ToolResult {
3030 call_id: call.id.clone(),
3031 output: Content::Text(
3032 "not executed: an earlier call in this batch failed fatally and the executor \
3033 stopped. This call did not take effect — re-issue it only if the failure it \
3034 followed does not make it pointless."
3035 .to_string(),
3036 ),
3037 is_error: true,
3038 is_fatal: false,
3039 error_kind: Some(ToolErrorKind::Fatal),
3040 token_count: None,
3041 })
3042 .collect())
3043 }
3044
3045 fn plan_effect_failure(
3053 &mut self,
3054 context: &PlanContext<'_>,
3055 effect_id: &EffectId,
3056 failure: &HostEffectFailure,
3057 ) -> Result<PlannedStep, KernelFault> {
3058 let Some(pending) = context.resolving else {
3059 return Err(unowned_resolution(effect_id, "effect"));
3060 };
3061 let tag = pending.tag();
3062 match tag {
3063 EffectKindTag::CallProvider => {
3066 self.provider_calls.remove(effect_id);
3067 self.host_effect_terminal(tag, failure)
3068 }
3069 EffectKindTag::EvaluateMilestone => self.host_effect_terminal(tag, failure),
3072 EffectKindTag::ExecuteTools => {
3075 let root_kind = self.require_root_kind()?;
3076 let engine = self.engine_mut()?;
3077 let results = engine
3078 .dispatched_tool_calls()
3079 .iter()
3080 .map(|call| ToolResult {
3081 call_id: call.id.clone(),
3082 output: Content::Text(format!(
3083 "not executed: the executor could not run this batch ({}). The call \
3084 did not take effect — try a different approach or a smaller step.",
3085 failure.kind.as_str()
3086 )),
3087 is_error: true,
3088 is_fatal: false,
3089 error_kind: Some(ToolErrorKind::Fatal),
3090 token_count: None,
3091 })
3092 .collect();
3093 let action = engine.feed(LoopEvent::ToolResults { results });
3094 self.continue_after(context, action, root_kind)
3095 }
3096 EffectKindTag::RequestApproval => {
3099 let root_kind = self.require_root_kind()?;
3100 let engine = self.engine_mut()?;
3101 let turn = engine.turn;
3102 let action = engine.resolve_approval(Vec::new(), Vec::new());
3105 engine
3106 .observations
3107 .push(KernelObservation::ApprovalResolutionFailed {
3108 turn,
3109 error: host_failure_text(failure),
3110 });
3111 self.continue_after(context, action, root_kind)
3112 }
3113 EffectKindTag::SpawnTasks => {
3116 let root_kind = self.require_root_kind()?;
3117 let error = host_failure_text(failure);
3118 let engine = self.engine_mut()?;
3119 let failures: Vec<WorkflowSpawnFailure> = engine
3120 .pending_spawn_agent_ids()
3121 .into_iter()
3122 .map(|agent_id| WorkflowSpawnFailure {
3123 agent_id,
3124 error: error.clone(),
3125 })
3126 .collect();
3127 for failed in &failures {
3128 self.attempts.remove(&failed.agent_id);
3129 }
3130 let action = self
3131 .engine_mut()?
3132 .resolve_workflow_spawn(Vec::new(), failures);
3133 self.continue_after(context, action, root_kind)
3134 }
3135 EffectKindTag::PreemptTasks => {
3138 self.require_root_kind()?;
3140 let error = host_failure_text(failure);
3141 let engine = self.engine_mut()?;
3142 let turn = engine.turn;
3143 let agent_ids: Vec<String> = match &pending.effect {
3144 EffectKind::PreemptTasks(preempt) => preempt
3145 .attempts
3146 .iter()
3147 .map(|attempt| attempt.task_id.as_str().to_string())
3148 .collect(),
3149 _ => Vec::new(),
3150 };
3151 engine
3152 .observations
3153 .push(KernelObservation::AgentPreemptFailed {
3154 turn,
3155 agent_ids,
3156 reason: match &pending.effect {
3157 EffectKind::PreemptTasks(preempt) => preempt.reason.clone(),
3158 _ => String::new(),
3159 },
3160 error,
3161 });
3162 Ok(self.quiet_step())
3163 }
3164 EffectKindTag::PersistMemory => {
3165 self.commit_memory_write(effect_id, None, Some(host_failure_text(failure)))
3166 }
3167 EffectKindTag::QueryMemory => {
3171 let root_kind = self.require_root_kind()?;
3172 let Some(query) = self.pending_memory_queries.remove(effect_id) else {
3173 return Err(unowned_resolution(effect_id, "memory query"));
3174 };
3175 let error = host_failure_text(failure);
3176 let engine = self.engine_mut()?;
3177 let turn = engine.turn;
3178 let action = engine.resume_after_preload();
3179 engine
3180 .observations
3181 .push(KernelObservation::MemoryQueryFailed {
3182 turn,
3183 scope: binding_scope(&query.binding_id),
3184 query: query.text,
3185 error,
3186 });
3187 let mut step = self.continue_after(context, action, root_kind)?;
3188 step.focus = self.focus.clone();
3189 Ok(step)
3190 }
3191 EffectKindTag::ArchivePageOut => {
3194 let root_kind = self.require_root_kind()?;
3195 let action = self
3196 .engine_mut()?
3197 .abandon_page_out_archive(host_failure_text(failure));
3198 self.continue_after(context, action, root_kind)
3199 }
3200 EffectKindTag::LoadPayload => {
3205 let root_kind = self.require_root_kind()?;
3206 let handle_id = self
3207 .pending_payload_loads
3208 .remove(effect_id)
3209 .map(|pending| pending.handle_id)
3210 .unwrap_or_default();
3211 let error = host_failure_text(failure);
3212 let engine = self.engine_mut()?;
3213 let turn = engine.turn;
3214 let action = engine.resume_after_preload();
3215 engine
3216 .observations
3217 .push(KernelObservation::PayloadLoadFailed {
3218 turn,
3219 handle_id,
3220 error,
3221 });
3222 let mut step = self.continue_after(context, action, root_kind)?;
3223 step.focus = self.focus.clone();
3224 Ok(step)
3225 }
3226 }
3227 }
3228
3229 fn host_effect_terminal(
3231 &mut self,
3232 tag: EffectKindTag,
3233 failure: &HostEffectFailure,
3234 ) -> Result<PlannedStep, KernelFault> {
3235 let root_kind = self.require_root_kind()?;
3236 let usage = self.usage_report();
3237 if let Some(engine) = self.engine.as_mut() {
3240 engine.close_for_host_effect_failure();
3241 }
3242 Ok(PlannedStep {
3243 root_kind: Some(root_kind),
3244 focus: self.focus.clone(),
3245 observations: Vec::new(),
3246 disposition: StepDisposition::Terminal(TerminalDisposition {
3247 terminal: KernelTerminal::Failed(FailedTerminal {
3248 failure: KernelFailure {
3249 code: KernelFailureCode::HostEffectFailed,
3250 message: format!(
3251 "the host could not execute this operation's {tag} effect ({}){}",
3252 failure.kind.as_str(),
3253 if failure.message.is_empty() {
3254 String::new()
3255 } else {
3256 format!(": {}", failure.message)
3257 }
3258 ),
3259 },
3260 usage,
3261 }),
3262 }),
3263 })
3264 }
3265
3266 fn quiet_step(&self) -> PlannedStep {
3272 PlannedStep {
3273 root_kind: self.root_kind,
3274 focus: self.focus.clone(),
3275 observations: Vec::new(),
3276 disposition: StepDisposition::Effects(EffectsDisposition::default()),
3277 }
3278 }
3279
3280 fn commit_memory_write(
3284 &mut self,
3285 effect_id: &EffectId,
3286 receipt: Option<&super::effect::MemoryPersistReceipt>,
3287 failure: Option<String>,
3288 ) -> Result<PlannedStep, KernelFault> {
3289 self.require_root_kind()?;
3291 let Some(authored) = self.pending_memory_writes.remove(effect_id) else {
3292 return Err(unowned_resolution(effect_id, "memory write"));
3293 };
3294 let engine = self.engine_mut()?;
3295 let turn = engine.turn;
3296 match (receipt, failure) {
3297 (Some(receipt), _) => {
3298 engine.observations.push(KernelObservation::MemoryWritten {
3299 turn,
3300 record_id: receipt.record_ref.as_str().to_string(),
3301 scope: binding_scope(&authored.binding_id),
3302 memory_kind: core_memory_kind(authored.kind),
3303 name: authored.name,
3304 size_bytes: authored.size_bytes,
3305 });
3306 }
3307 (None, Some(error)) => {
3308 engine
3309 .observations
3310 .push(KernelObservation::MemoryWriteFailed {
3311 turn,
3312 record_id: authored.name,
3315 error,
3316 });
3317 }
3318 (None, None) => unreachable!("a memory resolution is either a receipt or a failure"),
3319 }
3320 Ok(self.quiet_step())
3321 }
3322
3323 fn commit_payload_load(
3333 &mut self,
3334 context: &PlanContext<'_>,
3335 effect_id: &EffectId,
3336 loaded: &super::effect::PayloadLoadedSuccess,
3337 ) -> Result<PlannedStep, KernelFault> {
3338 let root_kind = self.require_root_kind()?;
3339 let Some(pending) = self.pending_payload_loads.get(effect_id).cloned() else {
3340 return Err(unowned_resolution(effect_id, "payload load"));
3341 };
3342 let mismatch = |what: &str| {
3343 Err(KernelFault::new(
3344 KernelFaultCode::UnexpectedEffectOutcome,
3345 format!(
3346 "the payload loaded for effect {effect_id} is not the body the kernel paged \
3347 out: {what}"
3348 ),
3349 ))
3350 };
3351 if loaded.handle_id.as_str() != pending.handle_id {
3352 return mismatch(&format!(
3353 "it names handle {}, but the effect addressed {}",
3354 loaded.handle_id, pending.handle_id
3355 ));
3356 }
3357 let content = loaded.payload.content.as_str();
3358 if loaded.payload.original_size.get() != content.len() as u64 {
3359 return mismatch(&format!(
3360 "it declares {} bytes and carries {}",
3361 loaded.payload.original_size,
3362 content.len()
3363 ));
3364 }
3365 if let Some(original_size) = pending.original_size
3366 && loaded.payload.original_size.get() != original_size
3367 {
3368 return mismatch(&format!(
3369 "it carries {} bytes and the handle records {original_size}",
3370 loaded.payload.original_size
3371 ));
3372 }
3373 let digest = super::record::canonical_digest(content.as_bytes());
3374 if digest.as_str() != pending.digest {
3375 return mismatch(&format!(
3376 "its content digests to {digest}, and the handle records {}",
3377 pending.digest
3378 ));
3379 }
3380
3381 self.pending_payload_loads.remove(effect_id);
3383 let engine = self.engine_mut()?;
3384 let turn = engine.turn;
3385 let body = format!("[PAYLOAD handle_id={}]\n{content}", pending.handle_id);
3389 let tokens = engine.ctx.engine.count(&body).max(1);
3390 engine.ctx.push_history(Message::user(body), tokens);
3391 let previous = engine.ctx.set_payload_residency(
3392 &pending.handle_id,
3393 HandleKind::ToolResult,
3394 tokens,
3395 Residency::Resident,
3396 );
3397 let action = engine.resume_after_preload();
3398 engine
3399 .observations
3400 .push(KernelObservation::PayloadResidencyChanged {
3401 turn,
3402 handle_id: pending.handle_id.clone(),
3403 from: previous.map(|residency| residency.label().to_string()),
3404 to: "resident".to_string(),
3405 payload_ref: None,
3406 original_size: loaded.payload.original_size.get(),
3407 });
3408 let mut step = self.continue_after(context, action, root_kind)?;
3409 step.focus = self.focus.clone();
3410 Ok(step)
3411 }
3412
3413 fn record_external_payloads(
3420 &mut self,
3421 payloads: &[WireToolResultPayload],
3422 ) -> Result<(), KernelFault> {
3423 for payload in payloads {
3424 let WireToolResultPayload::External(external) = payload else {
3425 continue;
3426 };
3427 let engine = self.engine_mut()?;
3428 let turn = engine.turn;
3429 let previous = engine.ctx.set_payload_residency(
3430 external.call_id.as_str(),
3431 HandleKind::ToolResult,
3432 0,
3435 Residency::External {
3436 payload_ref: external.payload_ref.as_str().to_string(),
3437 digest: external.digest.as_str().to_string(),
3438 original_size: external.original_size.get(),
3439 },
3440 );
3441 engine
3442 .observations
3443 .push(KernelObservation::PayloadResidencyChanged {
3444 turn,
3445 handle_id: external.call_id.as_str().to_string(),
3446 from: previous.map(|residency| residency.label().to_string()),
3447 to: "external".to_string(),
3448 payload_ref: Some(external.payload_ref.as_str().to_string()),
3449 original_size: external.original_size.get(),
3450 });
3451 }
3452 Ok(())
3453 }
3454
3455 fn verify_page_out_receipt(
3459 &self,
3460 context: &PlanContext<'_>,
3461 effect_id: &EffectId,
3462 receipt: &super::effect::ArchiveReceipt,
3463 ) -> Result<(), KernelFault> {
3464 let Some(KernelEffect {
3465 effect: EffectKind::ArchivePageOut(published),
3466 ..
3467 }) = context.resolving
3468 else {
3469 return Err(unowned_resolution(effect_id, "page-out archive"));
3470 };
3471 if receipt.handle_id != published.handle_id
3472 || receipt.digest != published.payload.digest
3473 || receipt.original_size != published.payload.original_size
3474 {
3475 return Err(KernelFault::new(
3476 KernelFaultCode::UnexpectedEffectOutcome,
3477 format!(
3478 "the archive receipt for effect {effect_id} names handle {} / digest {}, but \
3479 the kernel published handle {} / digest {}",
3480 receipt.handle_id,
3481 receipt.digest,
3482 published.handle_id,
3483 published.payload.digest
3484 ),
3485 ));
3486 }
3487 Ok(())
3488 }
3489
3490 fn page_out_effect(
3492 &self,
3493 context: &PlanContext<'_>,
3494 summary: Option<&str>,
3495 archived: &[Message],
3496 effect_index: u32,
3497 ) -> Result<ArchivePageOutEffect, KernelFault> {
3498 let content = serde_json::to_string(archived).map_err(|error| {
3499 KernelFault::new(
3500 KernelFaultCode::MalformedEnvelope,
3501 format!("archived history is not serialisable: {error}"),
3502 )
3503 })?;
3504 let preview_bytes = context.config.payload_policy.preview_bytes as usize;
3505 let preview = summary
3506 .map(str::to_string)
3507 .unwrap_or_else(|| truncate_on_char_boundary(&content, preview_bytes));
3508 let handle_id = super::scalar::HandleId::new(format!(
3509 "{}:step:{}:page-out:{effect_index}",
3510 context.input.operation_id, context.step_seq
3511 ))
3512 .map_err(malformed)?;
3513 Ok(ArchivePageOutEffect {
3514 handle_id,
3515 payload: PageOutPayload {
3516 digest: super::record::canonical_digest(content.as_bytes()),
3517 original_size: WireU64::new(content.len() as u64),
3518 content,
3519 preview,
3520 },
3521 })
3522 }
3523
3524 fn require_pending_provider_call(
3525 &self,
3526 effect_id: &EffectId,
3527 ) -> Result<&PendingProviderCall, KernelFault> {
3528 self.provider_calls.get(effect_id).ok_or_else(|| {
3529 KernelFault::new(
3530 KernelFaultCode::InvalidAuthority,
3531 format!(
3532 "effect {effect_id} is not a provider call this kernel published, so its \
3533 result has no turn to continue (§7.6)"
3534 ),
3535 )
3536 })
3537 }
3538
3539 fn plan_external_event(
3542 &mut self,
3543 context: &PlanContext<'_>,
3544 event: &ExternalEvent,
3545 ) -> Result<PlannedStep, KernelFault> {
3546 match event {
3547 ExternalEvent::DeliverSignal(delivery) => self.plan_signal(context, delivery),
3548 ExternalEvent::ChildCompleted(completed) => {
3549 self.plan_child_completed(context, completed)
3550 }
3551 }
3552 }
3553
3554 fn plan_signal(
3577 &mut self,
3578 context: &PlanContext<'_>,
3579 delivery: &DeliverSignal,
3580 ) -> Result<PlannedStep, KernelFault> {
3581 let root_kind = self.require_root_kind()?;
3582 if delivery.attempt == 0 {
3583 return Err(KernelFault::new(
3584 KernelFaultCode::MalformedEnvelope,
3585 format!(
3586 "delivery {} carries attempt 0; attempts are 1-based, and a delivery that \
3587 cannot say which attempt it is cannot be told apart from a redelivery (§7.7)",
3588 delivery.delivery_id
3589 ),
3590 ));
3591 }
3592 if let SignalTarget::Task(target) = &delivery.signal.target {
3593 let live = self
3594 .engine
3595 .as_ref()
3596 .and_then(|engine| engine.task_lifecycle(target.task_id.as_str()))
3597 .is_some_and(|lifecycle| !lifecycle.is_terminal());
3598 if !live {
3599 return Err(KernelFault::new(
3600 KernelFaultCode::InvalidAuthority,
3601 format!(
3602 "signal {} targets task {}, which this operation has no live attempt for; \
3603 a signal addresses the operation or one of its own logical tasks (§7.7)",
3604 delivery.signal.signal_id, target.task_id
3605 ),
3606 ));
3607 }
3608 }
3609 let escalation_enabled = context.config.signal_policy.deadline_escalation;
3621 if delivery.signal.effective_urgency(escalation_enabled) == SignalUrgency::Critical
3622 && !self.attempts.is_empty()
3623 {
3624 self.require_effect_support(context.config, EffectKindTag::PreemptTasks)?;
3625 }
3626
3627 let may_issue_request = self.provider_calls.is_empty();
3631 let signal = runtime_signal(&delivery.signal, context.input.observed_at_ms);
3632 let engine = self.engine_mut()?;
3633 let action = engine.signal_event(
3634 context.input.operation_id.as_str().to_string(),
3635 delivery.delivery_id.as_str().to_string(),
3636 delivery.attempt,
3637 signal,
3638 may_issue_request,
3639 );
3640 match action {
3641 Some(action) => self.continue_after(context, action, root_kind),
3642 None => Ok(self.quiet_step()),
3645 }
3646 }
3647
3648 fn plan_child_completed(
3650 &mut self,
3651 context: &PlanContext<'_>,
3652 completed: &ChildCompleted,
3653 ) -> Result<PlannedStep, KernelFault> {
3654 let root_kind = self.require_root_kind()?;
3655 match self.attempts.get(completed.task_id.as_str()) {
3656 Some(minted) if minted == &completed.attempt_id => {}
3657 issued => {
3658 return Err(KernelFault::new(
3659 KernelFaultCode::InvalidAuthority,
3660 format!(
3661 "task {} has attempt {} in this kernel, but the completion names {}; a \
3662 host does not mint or rewrite child identity (§10.4)",
3663 completed.task_id,
3664 issued.map_or("none", AttemptId::as_str),
3665 completed.attempt_id,
3666 ),
3667 ));
3668 }
3669 }
3670
3671 self.attempts.remove(completed.task_id.as_str());
3675
3676 let mut index = 0u32;
3687 let mut effects = Vec::new();
3688 for (seq, request) in completed.parent_requests.iter().enumerate() {
3689 let causation = SyscallCausation::ChildAttempt(ChildAttemptCausation {
3690 task_id: completed.task_id.clone(),
3691 attempt_id: completed.attempt_id.clone(),
3692 request_seq: seq as u32,
3693 });
3694 match self.apply_syscall(context, &causation, request, &mut index) {
3695 Ok(outcome) => effects.extend(outcome.effects),
3696 Err(SyscallRefusal::Fault(fault)) => self.note_rejection(
3697 SyscallRejection::new(
3698 "parent_request",
3699 format!("request {seq} refused: {}", fault.message),
3700 )
3701 .by(&completed.task_id),
3702 ),
3703 Err(SyscallRefusal::Rejected(rejection)) => {
3704 self.note_rejection(rejection.by(&completed.task_id))
3705 }
3706 }
3707 }
3708
3709 let syscall_observations = self
3713 .engine_mut()?
3714 .take_observations()
3715 .into_iter()
3716 .collect::<Vec<_>>();
3717
3718 let result = sub_agent_result(completed);
3719 let engine = self.engine_mut()?;
3720 let action = engine.feed(LoopEvent::SubAgentCompleted { result });
3721 let mut step = self.continue_after_at(context, action, root_kind, &mut index)?;
3722 if let Some(engine) = self.engine.as_mut() {
3723 engine.observations.splice(0..0, syscall_observations);
3724 }
3725 if !effects.is_empty() {
3726 match &mut step.disposition {
3727 StepDisposition::Effects(published) => {
3728 let mut merged = effects;
3729 merged.append(&mut published.effects);
3730 published.effects = merged;
3731 }
3732 StepDisposition::Terminal(_) => {
3733 return Err(KernelFault::new(
3734 KernelFaultCode::InvalidLifecycle,
3735 "a completion that terminates the operation cannot also publish the \
3736 effects its parent requests asked for (§7.12)"
3737 .to_string(),
3738 ));
3739 }
3740 }
3741 }
3742 Ok(step)
3743 }
3744
3745 fn plan_host_control(
3759 &mut self,
3760 context: &PlanContext<'_>,
3761 command: &HostCommand,
3762 ) -> Result<PlannedStep, KernelFault> {
3763 match command {
3764 HostCommand::Cancel(cancel) => self.plan_cancel(context, cancel),
3765 HostCommand::ForceCompact(_) => {
3766 let root_kind = self.require_root_kind()?;
3767 self.engine_mut()?.force_compact();
3768 self.continue_after(context, LoopAction::AwaitingResume, root_kind)
3771 }
3772 HostCommand::UpdateTask(update) => self.plan_host_task_update(update),
3773 HostCommand::ApplyCapabilityPatch(patch) => self.plan_capability_patch(patch),
3774 HostCommand::ApplyKnowledgeMutation(mutation) => self.plan_knowledge_mutation(mutation),
3775 HostCommand::SeedKnowledge(seed) => self.plan_seed_knowledge(seed),
3776 HostCommand::ApplySkillActivation(activation) => self.plan_skill_activation(activation),
3777 HostCommand::ApplyPolicyPatch(patch) => self.plan_policy_patch(patch),
3778 HostCommand::UpdateDeadline(deadline) => self.plan_update_deadline(deadline),
3779 }
3780 }
3781
3782 fn plan_cancel(
3802 &mut self,
3803 context: &PlanContext<'_>,
3804 cancel: &CancelCommand,
3805 ) -> Result<PlannedStep, KernelFault> {
3806 let root_kind = self.root_kind;
3807 let focus = self.focus.clone();
3808 let reason = cancel.reason;
3809 let operation_id = context.input.operation_id.as_str().to_string();
3811 let engine = self.engine_mut()?;
3812 let action = engine.cancel_operation(
3813 operation_id,
3814 reason,
3815 cancel
3816 .pending_call_ids
3817 .iter()
3818 .map(|call_id| call_id.as_str().to_string())
3819 .collect(),
3820 );
3821 self.attempts.clear();
3823 self.provider_calls.clear();
3824 self.pending_memory_writes.clear();
3825 self.pending_memory_queries.clear();
3826
3827 let LoopAction::Done { result } = action else {
3828 return Err(KernelFault::new(
3829 KernelFaultCode::InvalidLifecycle,
3830 format!(
3831 "cancelling the operation produced {} instead of a terminal; cancellation is \
3832 the one control command that always ends the operation (§11.1)",
3833 loop_action_label(&action)
3834 ),
3835 ));
3836 };
3837 Ok(PlannedStep {
3838 root_kind,
3839 focus,
3840 observations: Vec::new(),
3841 disposition: StepDisposition::Terminal(TerminalDisposition {
3842 terminal: KernelTerminal::Cancelled(CancelledTerminal {
3843 reason,
3844 usage: UsageReport {
3845 input_tokens: WireU64::new(result.total_tokens_used),
3846 output_tokens: WireU64::ZERO,
3847 turns: result.turns_used,
3848 cached_input_tokens: None,
3849 },
3850 }),
3851 }),
3852 })
3853 }
3854
3855 fn plan_host_task_update(
3859 &mut self,
3860 update: &UpdateTaskCommand,
3861 ) -> Result<PlannedStep, KernelFault> {
3862 let engine = self.engine_mut()?;
3863 engine.ctx.update_task(core_task_update(&update.update));
3864 Ok(self.quiet_step())
3865 }
3866
3867 fn plan_capability_patch(
3870 &mut self,
3871 patch: &ApplyCapabilityPatchCommand,
3872 ) -> Result<PlannedStep, KernelFault> {
3873 let engine = self.engine_mut()?;
3874 for grant in &patch.patch.mount {
3875 engine.mount_capability(
3876 crate::types::capability::CapabilityDescriptor {
3877 id: grant.id.as_str().into(),
3878 kind: core_capability_kind(grant.kind),
3879 description: grant.description.clone().unwrap_or_default(),
3880 tool_schema: None,
3881 skill: None,
3882 metadata: serde_json::Value::Null,
3883 lease: None,
3884 is_pinned: false,
3885 version: None,
3886 mounted_by: None,
3887 mount_reason: None,
3888 },
3889 None,
3890 None,
3891 );
3892 }
3893 for reference in &patch.patch.unmount {
3894 engine.unmount_capability(core_capability_kind(reference.kind), &reference.id);
3895 }
3896 Ok(self.quiet_step())
3897 }
3898
3899 fn plan_knowledge_mutation(
3902 &mut self,
3903 mutation: &ApplyKnowledgeMutationCommand,
3904 ) -> Result<PlannedStep, KernelFault> {
3905 let engine = self.engine_mut()?;
3906 seed_knowledge(engine, &mutation.mutation.upsert);
3907 for key in &mutation.mutation.remove {
3908 engine.ctx.remove_knowledge(key);
3909 }
3910 Ok(self.quiet_step())
3911 }
3912
3913 fn plan_seed_knowledge(
3917 &mut self,
3918 seed: &SeedKnowledgeCommand,
3919 ) -> Result<PlannedStep, KernelFault> {
3920 let engine = self.engine_mut()?;
3921 seed_knowledge(engine, &seed.entries);
3922 Ok(self.quiet_step())
3923 }
3924
3925 fn plan_skill_activation(
3931 &mut self,
3932 activation: &ApplySkillActivationCommand,
3933 ) -> Result<PlannedStep, KernelFault> {
3934 let engine = self.engine_mut()?;
3935 for activate in &activation.activate {
3936 if !engine.ctx.skill_available(&activate.name) {
3937 return Err(KernelFault::new(
3938 KernelFaultCode::InvalidConfig,
3939 format!(
3940 "this operation declares no skill named {:?}; activating one is a \
3941 capability mutation and is refused rather than invented (§13.2)",
3942 activate.name
3943 ),
3944 ));
3945 }
3946 }
3947 let turn = engine.turn;
3948 for activate in &activation.activate {
3949 let expires_at_turn = activate.lease_turns.map(|turns| turn.saturating_add(turns));
3950 engine
3951 .ctx
3952 .activate_skill_leased(activate.name.as_str(), expires_at_turn);
3953 }
3954 for name in &activation.deactivate {
3955 engine.ctx.deactivate_skill(name);
3956 }
3957 Ok(self.quiet_step())
3958 }
3959
3960 fn plan_policy_patch(
3968 &mut self,
3969 patch: &ApplyPolicyPatchCommand,
3970 ) -> Result<PlannedStep, KernelFault> {
3971 let Some(policy) = self.policy.as_mut() else {
3972 return Err(KernelFault::new(
3973 KernelFaultCode::InvalidLifecycle,
3974 "the operation has no genesis configuration, so it has no policy to patch"
3975 .to_string(),
3976 ));
3977 };
3978 let revision = policy.apply(patch).map_err(|rejection| {
3979 KernelFault::new(KernelFaultCode::InvalidConfig, rejection.message)
3980 })?;
3981 let config = policy.config().clone();
3982 let engine = self.engine_mut()?;
3983 install_live_policies(engine, &config);
3984 let turn = engine.turn;
3985 engine
3986 .observations
3987 .push(KernelObservation::LivePolicyChanged {
3988 turn,
3989 policy: live_policy_label(&patch.patch).to_string(),
3990 revision: revision.get(),
3991 });
3992 Ok(self.quiet_step())
3993 }
3994
3995 fn plan_update_deadline(
4003 &mut self,
4004 deadline: &UpdateDeadlineCommand,
4005 ) -> Result<PlannedStep, KernelFault> {
4006 let engine = self.engine_mut()?;
4007 let started_at_ms = engine.started_at_ms();
4008 let budget = match (deadline.deadline_ms, started_at_ms) {
4009 (None, _) => None,
4010 (Some(deadline_ms), Some(started_at_ms)) => {
4011 Some(deadline_ms.get().saturating_sub(started_at_ms))
4012 }
4013 (Some(_), None) => {
4014 return Err(KernelFault::new(
4015 KernelFaultCode::InvalidLifecycle,
4016 "this operation has accepted no timed input yet, so an absolute deadline has \
4017 no start to measure from (§11.2)"
4018 .to_string(),
4019 ));
4020 }
4021 };
4022 engine.set_wall_budget(budget);
4023 Ok(self.quiet_step())
4024 }
4025
4026 fn continue_after(
4029 &mut self,
4030 context: &PlanContext<'_>,
4031 action: LoopAction,
4032 root_kind: RootKind,
4033 ) -> Result<PlannedStep, KernelFault> {
4034 let mut index = 0;
4035 self.continue_after_at(context, action, root_kind, &mut index)
4036 }
4037
4038 fn continue_after_at(
4039 &mut self,
4040 context: &PlanContext<'_>,
4041 action: LoopAction,
4042 root_kind: RootKind,
4043 effect_index: &mut u32,
4044 ) -> Result<PlannedStep, KernelFault> {
4045 let workflow_finished = self
4046 .engine()
4047 .map(|engine| {
4048 engine
4049 .observations
4050 .iter()
4051 .any(|o| matches!(o, KernelObservation::WorkflowCompleted { .. }))
4052 })
4053 .unwrap_or(false);
4054 let disposition = self.disposition_for_at(context, action, root_kind, effect_index)?;
4055 let focus = if workflow_finished {
4056 match (root_kind, &self.focus) {
4059 (RootKind::Agent, Some(ExecutionFocus::WorkflowController(controller))) => {
4060 controller
4061 .parent_task_id
4062 .clone()
4063 .map(ExecutionFocus::agent_turn)
4064 }
4065 (_, current) => current.clone(),
4066 }
4067 } else {
4068 self.focus.clone()
4069 };
4070 Ok(PlannedStep {
4071 root_kind: Some(root_kind),
4072 focus,
4073 observations: Vec::new(),
4074 disposition,
4075 })
4076 }
4077
4078 fn disposition_for(
4080 &mut self,
4081 context: &PlanContext<'_>,
4082 action: LoopAction,
4083 root_kind: RootKind,
4084 ) -> Result<StepDisposition, KernelFault> {
4085 let mut index = 0;
4086 self.disposition_for_at(context, action, root_kind, &mut index)
4087 }
4088
4089 fn disposition_for_at(
4092 &mut self,
4093 context: &PlanContext<'_>,
4094 action: LoopAction,
4095 root_kind: RootKind,
4096 effect_index: &mut u32,
4097 ) -> Result<StepDisposition, KernelFault> {
4098 let operation_id = &context.input.operation_id;
4099 let causation = context.input.input_id.clone();
4100 let step_seq = context.step_seq;
4101
4102 let mut action = self.engine_mut()?.externalize_pending_host_effect(action);
4106 while matches!(action, LoopAction::ArchivePageOut { .. })
4111 && !context
4112 .config
4113 .host_effect_support
4114 .supports(EffectKindTag::ArchivePageOut)
4115 {
4116 action = self.engine_mut()?.abandon_page_out_archive(
4117 "this operation's host declares no archive_page_out support".to_string(),
4118 );
4119 }
4120
4121 let effect = match action {
4122 LoopAction::AwaitingResume => {
4123 if root_kind == RootKind::Workflow
4125 && let Some(terminal) = self.root_workflow_terminal()
4126 {
4127 return Ok(StepDisposition::Terminal(TerminalDisposition { terminal }));
4128 }
4129 return Ok(StepDisposition::Effects(EffectsDisposition::default()));
4130 }
4131 LoopAction::Done { result } => {
4132 return Ok(StepDisposition::Terminal(TerminalDisposition {
4133 terminal: agent_terminal(&result),
4134 }));
4135 }
4136 LoopAction::CallLLM {
4137 context: rendered,
4138 tools,
4139 } => EffectKind::CallProvider(CallProviderEffect {
4140 context: rendered_context(&rendered),
4141 tools: tools.iter().map(tool_schema).collect(),
4142 }),
4143 LoopAction::ExecuteTools { calls } => EffectKind::ExecuteTools(ExecuteToolsEffect {
4144 calls: calls.iter().map(wire_tool_call).collect::<Result<_, _>>()?,
4145 }),
4146 LoopAction::RequestApproval { requests } => {
4147 EffectKind::RequestApproval(RequestApprovalEffect {
4148 requests: requests
4149 .iter()
4150 .map(wire_approval_request)
4151 .collect::<Result<_, _>>()?,
4152 })
4153 }
4154 LoopAction::SpawnWorkflow { nodes, budget } => {
4155 let mut tasks = Vec::with_capacity(nodes.len());
4156 for node in &nodes {
4157 tasks.push(self.task_launch(operation_id, step_seq, node)?);
4158 }
4159 EffectKind::SpawnTasks(SpawnTasksEffect {
4160 tasks,
4161 budget: budget.as_ref().map(workflow_budget),
4162 })
4163 }
4164 LoopAction::PreemptSubAgents { agent_ids, reason } => {
4165 let attempts = agent_ids
4169 .iter()
4170 .filter_map(|agent_id| {
4171 let attempt_id = self.attempts.get(agent_id)?.clone();
4172 let task_id = TaskId::new(agent_id).ok()?;
4173 Some(TaskAttemptRef {
4174 task_id,
4175 attempt_id,
4176 })
4177 })
4178 .collect();
4179 EffectKind::PreemptTasks(PreemptTasksEffect { attempts, reason })
4180 }
4181 LoopAction::EvaluateMilestone {
4188 phase_id,
4189 criteria: _,
4190 required_evidence: _,
4191 verifier: _,
4192 } => EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
4193 request: super::effect::MilestoneRequest {
4194 contract_id: self.require_loaded_contract()?,
4195 phase_id,
4196 },
4197 }),
4198 LoopAction::ArchivePageOut {
4199 summary, archived, ..
4200 } => EffectKind::ArchivePageOut(self.page_out_effect(
4201 context,
4202 summary.as_deref(),
4203 &archived,
4204 *effect_index,
4205 )?),
4206 action @ (LoopAction::PersistMemory { .. } | LoopAction::QueryMemory { .. }) => {
4210 return Err(KernelFault::new(
4211 KernelFaultCode::InvalidLifecycle,
4212 format!(
4213 "the semantic kernel emitted {}, which only the deleted legacy memory \
4214 inputs can produce; on the canonical wire a memory effect is minted by \
4215 the P1 syscall that proposed it",
4216 loop_action_label(&action)
4217 ),
4218 ));
4219 }
4220 };
4221
4222 let tag = effect.tag();
4223 self.require_effect_support(context.config, tag)?;
4224 let effect_id = mint_effect_id(operation_id, step_seq, *effect_index);
4225 *effect_index += 1;
4226
4227 match &effect {
4228 EffectKind::CallProvider(call) => {
4231 self.provider_calls.insert(
4232 effect_id.clone(),
4233 PendingProviderCall {
4234 task_id: self.turn_task_id(),
4235 exposed_tools: call.tools.iter().map(|tool| tool.name.clone()).collect(),
4236 },
4237 );
4238 }
4239 EffectKind::SpawnTasks(spawn) => {
4242 let launched: Vec<String> = spawn
4243 .tasks
4244 .iter()
4245 .map(|task| task.task_id.as_str().to_string())
4246 .collect();
4247 if let Some(engine) = self.engine.as_mut() {
4248 engine.mark_tasks_starting(&launched);
4249 }
4250 }
4251 _ => {}
4252 }
4253
4254 Ok(StepDisposition::Effects(EffectsDisposition {
4255 effects: vec![KernelEffect {
4256 effect_id,
4257 causation_input_id: causation,
4258 effect,
4259 }],
4260 }))
4261 }
4262
4263 fn turn_task_id(&self) -> TaskId {
4267 let staged = self
4268 .staged
4269 .as_ref()
4270 .and_then(|staged| staged.focus.as_ref());
4271 match staged.or(self.focus.as_ref()) {
4272 Some(ExecutionFocus::AgentTurn(turn)) => turn.task_id.clone(),
4273 Some(ExecutionFocus::WorkflowController(controller)) => controller
4274 .parent_task_id
4275 .clone()
4276 .unwrap_or_else(root_task_id),
4277 None => root_task_id(),
4278 }
4279 }
4280
4281 fn root_workflow_terminal(&mut self) -> Option<KernelTerminal> {
4285 let workflow_id = self.workflow_id.clone()?;
4286 let engine = self.engine.as_mut()?;
4287 let outcomes = engine
4288 .observations
4289 .iter()
4290 .find_map(|observation| match observation {
4291 KernelObservation::WorkflowCompleted { node_outcomes, .. } => {
4292 Some(node_outcomes.clone())
4293 }
4294 _ => None,
4295 })?;
4296 let mut completed = Vec::new();
4297 let mut failed = Vec::new();
4298 for outcome in &outcomes {
4299 let node_id = self.node_id_for(&outcome.node_id);
4300 match outcome.status {
4301 WorkflowNodeStatus::Completed | WorkflowNodeStatus::CompletedPartial => {
4302 completed.push(node_id)
4303 }
4304 WorkflowNodeStatus::Failed | WorkflowNodeStatus::SkippedUpstreamFailed => {
4305 failed.push(node_id)
4306 }
4307 }
4308 }
4309 let status = if failed.is_empty() {
4310 WorkflowStatus::Completed
4311 } else {
4312 WorkflowStatus::Failed
4313 };
4314 Some(KernelTerminal::Workflow(WorkflowTerminal {
4315 outcome: WorkflowOutcome {
4316 workflow_id,
4317 status,
4318 completed_nodes: completed,
4319 failed_nodes: failed,
4320 },
4321 usage: self.usage_report(),
4322 }))
4323 }
4324
4325 fn usage_report(&self) -> UsageReport {
4326 let Some(engine) = self.engine.as_ref() else {
4327 return UsageReport::default();
4328 };
4329 let (tokens, _, _) = engine.local_budget_usage();
4330 UsageReport {
4331 input_tokens: WireU64::new(tokens),
4332 output_tokens: WireU64::ZERO,
4333 turns: engine.turn,
4334 cached_input_tokens: None,
4335 }
4336 }
4337
4338 fn task_launch(
4341 &mut self,
4342 operation_id: &OperationId,
4343 step_seq: WireU64,
4344 info: &crate::orchestration::workflow::WorkflowSpawnInfo,
4345 ) -> Result<TaskLaunch, KernelFault> {
4346 let task_id = TaskId::new(&info.agent_id).map_err(malformed)?;
4347 let attempt_id =
4348 AttemptId::new(format!("{}:attempt:1", info.agent_id)).map_err(malformed)?;
4349 let launch_token = LaunchToken::new(format!(
4350 "{operation_id}:step:{step_seq}:launch:{}",
4351 info.agent_id
4352 ))
4353 .map_err(malformed)?;
4354 self.attempts
4355 .insert(info.agent_id.clone(), attempt_id.clone());
4356 let mut metadata = serde_json::Map::new();
4357 if let Some(model_hint) = &info.model_hint {
4358 metadata.insert(
4359 "model_hint".to_string(),
4360 serde_json::Value::String(model_hint.clone()),
4361 );
4362 }
4363 if let Some(output_schema) = &info.output_schema {
4364 metadata.insert("output_schema".to_string(), output_schema.clone());
4365 }
4366 if !info.input_agent_ids.is_empty() {
4367 metadata.insert(
4368 "input_agent_ids".to_string(),
4369 serde_json::Value::Array(
4370 info.input_agent_ids
4371 .iter()
4372 .cloned()
4373 .map(serde_json::Value::String)
4374 .collect(),
4375 ),
4376 );
4377 let dependency_outputs = info
4378 .input_agent_ids
4379 .iter()
4380 .filter_map(|agent_id| {
4381 let output = self
4382 .engine
4383 .as_ref()?
4384 .task_table()
4385 .get(agent_id)?
4386 .proc
4387 .as_ref()?
4388 .result
4389 .as_ref()?
4390 .result
4391 .final_message
4392 .as_ref()
4393 .and_then(message_body_parts)?
4394 .0;
4395 Some((agent_id.clone(), serde_json::Value::String(output)))
4396 })
4397 .collect();
4398 metadata.insert(
4399 "dependency_outputs".to_string(),
4400 serde_json::Value::Object(dependency_outputs),
4401 );
4402 }
4403 Ok(TaskLaunch {
4404 task_id,
4405 attempt_id,
4406 launch_token,
4407 node_id: self.node_id_for(&info.agent_id),
4408 spec: LogicalAgentSpec {
4409 goal: info.goal.clone(),
4410 role: parse_wire_role(&info.role),
4411 isolation: parse_wire_isolation(&info.isolation),
4412 context_inheritance: parse_wire_context_inheritance(&info.context_inheritance),
4413 verification_contract_id: None,
4414 capability_filter: Default::default(),
4415 exposure_baseline: None,
4416 loop_round: None,
4417 metadata: super::scalar::BoundedJson::new(serde_json::Value::Object(metadata))
4418 .map_err(malformed)?,
4419 },
4420 })
4421 }
4422
4423 fn node_id_for(&self, agent_id: &str) -> NodeId {
4426 parse_node_index(agent_id)
4427 .and_then(|index| self.node_ids.get(index).cloned())
4428 .unwrap_or_else(|| {
4429 NodeId::new(agent_id).expect("an internal agent id is a legal branded ref")
4430 })
4431 }
4432
4433 fn require_known_contract(
4440 &self,
4441 config: &ResolvedOperationConfig,
4442 spec: Option<&LogicalAgentSpec>,
4443 ) -> Result<(), KernelFault> {
4444 let Some(contract_id) = spec.and_then(|spec| spec.verification_contract_id.as_deref())
4445 else {
4446 return Ok(());
4447 };
4448 if config.verification_contract(contract_id).is_some() {
4449 return Ok(());
4450 }
4451 Err(KernelFault::new(
4452 KernelFaultCode::InvalidConfig,
4453 format!(
4454 "the run spec names verification contract {contract_id:?}, which this operation's \
4455 catalog does not declare; a contract reference that resolves to nothing is a \
4456 milestone gate the run believes it has (§7.3)"
4457 ),
4458 ))
4459 }
4460
4461 fn load_verification_contract(
4467 &mut self,
4468 config: &ResolvedOperationConfig,
4469 spec: Option<&LogicalAgentSpec>,
4470 ) -> Result<(), KernelFault> {
4471 let Some(contract) = spec
4472 .and_then(|spec| spec.verification_contract_id.as_deref())
4473 .and_then(|id| config.verification_contract(id))
4474 else {
4475 return Ok(());
4476 };
4477 self.require_effect_support(config, EffectKindTag::EvaluateMilestone)?;
4480 let contract_id = contract.contract_id.clone();
4481 let cascade = core_milestone_contract(contract, config);
4482 self.engine_mut()?.load_milestone_contract(cascade);
4483 self.loaded_contract_id = Some(contract_id);
4486 Ok(())
4487 }
4488
4489 fn require_loaded_contract(&self) -> Result<String, KernelFault> {
4496 self.loaded_contract_id.clone().ok_or_else(|| {
4497 KernelFault::new(
4498 KernelFaultCode::InvalidLifecycle,
4499 "the semantic kernel asked for a milestone verdict, but this operation installed \
4500 no verification contract; a milestone request names the (contract_id, phase_id) \
4501 pair the host looks its verifier up by (§7.8)"
4502 .to_string(),
4503 )
4504 })
4505 }
4506
4507 fn require_effect_support(
4508 &self,
4509 config: &ResolvedOperationConfig,
4510 tag: EffectKindTag,
4511 ) -> Result<(), KernelFault> {
4512 if config.host_effect_support.supports(tag) {
4513 return Ok(());
4514 }
4515 Err(KernelFault::new(
4516 KernelFaultCode::UnsupportedEffect,
4517 format!(
4518 "this operation's host does not declare support for {tag} effects, so the \
4519 transition that would publish one is refused before anything moves"
4520 ),
4521 ))
4522 }
4523
4524 fn require_root_kind(&self) -> Result<RootKind, KernelFault> {
4525 self.root_kind.ok_or_else(|| {
4526 KernelFault::new(
4527 KernelFaultCode::InvalidLifecycle,
4528 "no root has started, so there is nothing to advance".to_string(),
4529 )
4530 })
4531 }
4532
4533 fn engine_mut(&mut self) -> Result<&mut LoopStateMachine, KernelFault> {
4534 self.engine.as_mut().ok_or_else(|| {
4535 KernelFault::new(
4536 KernelFaultCode::InvalidLifecycle,
4537 "the operation has no genesis configuration, so it has no semantic kernel to drive"
4538 .to_string(),
4539 )
4540 })
4541 }
4542
4543 fn poison_with(&mut self, fault: KernelFault) -> KernelFault {
4544 self.staged = None;
4545 self.poison.get_or_insert(fault).clone()
4546 }
4547}
4548
4549fn root_task_id() -> TaskId {
4554 TaskId::new(ROOT_TASK_ID).expect("the root task id is a legal branded ref")
4555}
4556
4557pub const SYSCALL_TOOL_NAMES: &[&str] = &[
4576 "start_workflow",
4577 "submit_workflow_nodes",
4578 "skill",
4579 "update_plan",
4580 crate::context::manager::MEMORY_TOOL_NAME,
4581 crate::context::manager::READ_RESULT_TOOL_NAME,
4582];
4583
4584fn is_syscall_tool(name: &str) -> bool {
4585 SYSCALL_TOOL_NAMES.contains(&name)
4586}
4587
4588fn decode_syscall(call: &WireToolCall) -> Result<SyscallRequest, SyscallRejection> {
4593 let arguments = call.arguments.get().clone();
4594 let name: &'static str = SYSCALL_TOOL_NAMES
4595 .iter()
4596 .copied()
4597 .find(|known| *known == call.name.as_str())
4598 .expect("only recognised syscall tools reach the decoder");
4599
4600 fn decode<T: serde::de::DeserializeOwned>(
4601 name: &'static str,
4602 arguments: serde_json::Value,
4603 ) -> Result<T, SyscallRejection> {
4604 serde_json::from_value(arguments)
4605 .map_err(|error| SyscallRejection::new(name, format!("malformed arguments: {error}")))
4606 }
4607
4608 match name {
4609 "start_workflow" => Ok(SyscallRequest::SubmitWorkflow(
4610 super::syscall::SubmitWorkflowRequest {
4611 spec: decode(name, arguments)?,
4612 },
4613 )),
4614 "submit_workflow_nodes" => {
4615 #[derive(serde::Deserialize)]
4616 struct Args {
4617 nodes: Vec<WireNode>,
4618 }
4619 let args: Args = decode(name, arguments)?;
4620 Ok(SyscallRequest::AppendWorkflowNodes(
4621 super::syscall::AppendWorkflowNodesRequest { nodes: args.nodes },
4622 ))
4623 }
4624 "skill" => {
4625 #[derive(serde::Deserialize)]
4626 struct Args {
4627 name: String,
4628 #[serde(default)]
4629 lease_turns: Option<u32>,
4630 }
4631 let args: Args = decode(name, arguments)?;
4632 Ok(SyscallRequest::ActivateSkill(
4633 super::syscall::ActivateSkillRequest {
4634 name: args.name,
4635 lease_turns: args.lease_turns,
4636 },
4637 ))
4638 }
4639 "update_plan" => Ok(SyscallRequest::UpdateTask(
4640 super::syscall::UpdateTaskRequest {
4641 update: decode(name, arguments)?,
4642 },
4643 )),
4644 crate::context::manager::MEMORY_TOOL_NAME => {
4645 #[derive(serde::Deserialize)]
4646 struct Args {
4647 #[serde(default)]
4648 query: String,
4649 #[serde(default)]
4650 kinds: Vec<WireMemoryKind>,
4651 #[serde(default)]
4652 top_k: Option<u32>,
4653 }
4654 let args: Args = decode(name, arguments)?;
4655 Ok(SyscallRequest::RequestMemoryQuery(
4656 super::syscall::RequestMemoryQueryRequest {
4657 query: super::syscall::MemoryQueryProposal {
4658 text: args.query,
4659 kinds: args.kinds,
4660 limit: args.top_k,
4661 },
4662 },
4663 ))
4664 }
4665 crate::context::manager::READ_RESULT_TOOL_NAME => {
4666 #[derive(serde::Deserialize)]
4667 struct Args {
4668 call_id: String,
4669 }
4670 let args: Args = decode(name, arguments)?;
4671 let handle_id = super::scalar::HandleId::new(args.call_id).map_err(|error| {
4672 SyscallRejection::new(name, format!("malformed handle: {}", error.message))
4673 })?;
4674 Ok(SyscallRequest::PageIn(super::syscall::PageInRequest {
4675 handle_id,
4676 }))
4677 }
4678 other => unreachable!("unrecognised syscall tool {other}"),
4679 }
4680}
4681
4682fn causation_task(causation: &SyscallCausation) -> TaskId {
4684 match causation {
4685 SyscallCausation::ProviderTool(provider) => provider.task_id.clone(),
4686 SyscallCausation::ChildAttempt(child) => child.task_id.clone(),
4687 }
4688}
4689
4690fn privileged_family(request: &SyscallRequest) -> Option<&'static str> {
4700 match request {
4701 SyscallRequest::SubmitWorkflow(_) | SyscallRequest::AppendWorkflowNodes(_) => {
4702 Some("workflow")
4703 }
4704 SyscallRequest::RequestMemoryWrite(_) | SyscallRequest::RequestMemoryQuery(_) => {
4705 Some("memory")
4706 }
4707 SyscallRequest::ActivateSkill(_) => Some("capability"),
4708 SyscallRequest::UpdateTask(_) | SyscallRequest::PageIn(_) => None,
4709 }
4710}
4711
4712fn core_task_update(update: &WireTaskUpdate) -> crate::context::task_state::TaskUpdate {
4713 crate::context::task_state::TaskUpdate {
4714 plan: update.plan.clone(),
4715 current_step: update.current_step.map(|step| step as usize),
4716 progress: update.progress.clone(),
4717 scratchpad: update.scratchpad.clone(),
4718 blocked_on: update.blocked_on.clone(),
4719 preserved_refs: update.preserved_refs.clone(),
4720 directives: update.directives.clone(),
4721 }
4722}
4723
4724fn mint_effect_id(operation_id: &OperationId, step_seq: WireU64, index: u32) -> EffectId {
4725 EffectId::new(format!("{operation_id}:step:{step_seq}:effect:{index}"))
4726 .expect("an operation-scoped effect id is always a legal branded ref")
4727}
4728
4729fn mint_workflow_id(operation_id: &OperationId, step_seq: WireU64) -> WorkflowId {
4730 WorkflowId::new(format!("{operation_id}:workflow:{step_seq}"))
4731 .expect("an operation-scoped workflow id is always a legal branded ref")
4732}
4733
4734fn parse_node_index(agent_id: &str) -> Option<usize> {
4736 let rest = agent_id.strip_prefix("wf-node")?;
4737 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
4738 digits.parse().ok()
4739}
4740
4741fn wire_node_ids(spec: &WireSpec) -> Vec<NodeId> {
4742 spec.nodes.iter().map(|node| node.node_id.clone()).collect()
4743}
4744
4745fn build_core_spec(spec: &WireSpec) -> Result<CoreWorkflowSpec, KernelFault> {
4748 let mut index_of: BTreeMap<&str, usize> = BTreeMap::new();
4749 for (index, node) in spec.nodes.iter().enumerate() {
4750 if index_of.insert(node.node_id.as_str(), index).is_some() {
4751 return Err(KernelFault::new(
4752 KernelFaultCode::InvalidConfig,
4753 format!(
4754 "workflow node id {:?} appears twice; node identity is unique within a DAG",
4755 node.node_id
4756 ),
4757 ));
4758 }
4759 }
4760 let mut nodes = Vec::with_capacity(spec.nodes.len());
4761 for node in &spec.nodes {
4762 let role = node
4763 .run_spec
4764 .as_ref()
4765 .and_then(|spec| spec.role)
4766 .map_or(AgentRole::Custom, core_role);
4767 let mut core = CoreWorkflowNode::new(runtime_task(&node.task), role);
4768 if let Some(isolation) = node.run_spec.as_ref().and_then(|spec| spec.isolation) {
4769 core = core.with_isolation(core_isolation(isolation));
4770 }
4771 if let Some(inheritance) = node
4772 .run_spec
4773 .as_ref()
4774 .and_then(|spec| spec.context_inheritance)
4775 {
4776 core.context_inheritance = core_context_inheritance(inheritance);
4777 }
4778 if let Some(metadata) = node
4779 .run_spec
4780 .as_ref()
4781 .map(|spec| spec.metadata.get())
4782 .and_then(serde_json::Value::as_object)
4783 {
4784 if let Some(model_hint) = metadata
4785 .get("model_hint")
4786 .and_then(serde_json::Value::as_str)
4787 {
4788 core = core.with_model_hint(model_hint);
4789 }
4790 if let Some(output_schema) = metadata.get("output_schema") {
4791 core = core.with_output_schema(output_schema.clone());
4792 }
4793 }
4794 let mut depends_on = Vec::with_capacity(node.depends_on.len());
4795 for dependency in &node.depends_on {
4796 let Some(&index) = index_of.get(dependency.as_str()) else {
4797 return Err(KernelFault::new(
4798 KernelFaultCode::InvalidConfig,
4799 format!(
4800 "workflow node {:?} depends on {:?}, which this DAG does not declare",
4801 node.node_id, dependency
4802 ),
4803 ));
4804 };
4805 depends_on.push(index);
4806 }
4807 nodes.push(core.with_depends_on(depends_on));
4808 }
4809 let core = CoreWorkflowSpec::new(nodes);
4810 core.validate()
4811 .map_err(|error| KernelFault::new(KernelFaultCode::InvalidConfig, error.to_string()))?;
4812 Ok(core)
4813}
4814
4815fn runtime_task(task: &LogicalTask) -> RuntimeTask {
4816 RuntimeTask {
4817 goal: task.goal.clone(),
4818 criteria: task.criteria.clone(),
4819 metadata: task.metadata.get().clone(),
4820 lane: task.lane.as_ref().map(TaskLane::new).unwrap_or_default(),
4821 }
4822}
4823
4824fn agent_run_spec(spec: &LogicalAgentSpec) -> AgentRunSpec {
4827 AgentRunSpec {
4828 identity: AgentIdentity::new(ROOT_TASK_ID, NO_HOST_SESSION),
4829 role: spec.role.map_or(AgentRole::Custom, core_role),
4830 isolation: spec
4831 .isolation
4832 .map_or(AgentIsolation::Shared, core_isolation),
4833 goal: spec.goal.clone(),
4834 verification_contract_id: spec.verification_contract_id.as_deref().map(Into::into),
4835 capability_filter: AgentCapabilityFilter {
4836 allowed_kinds: Vec::new(),
4837 allowed_ids: spec
4838 .capability_filter
4839 .allowed_ids
4840 .iter()
4841 .map(|id| id.as_str().into())
4842 .collect(),
4843 },
4844 milestones: None,
4845 metadata: spec.metadata.get().clone(),
4846 loop_round: spec.loop_round.as_ref().map(|round| LoopRoundSpec {
4847 max_rounds: round.max_rounds,
4848 min_sleep_ms: round.min_sleep_ms.map(WireU64::get),
4849 max_sleep_ms: round.max_sleep_ms.map(WireU64::get),
4850 default_action: round.default_action.clone(),
4851 }),
4852 exposure_baseline: spec
4853 .exposure_baseline
4854 .as_ref()
4855 .map(|ids| ids.iter().map(|id| id.as_str().into()).collect()),
4856 }
4857}
4858
4859fn core_role(role: WireRole) -> AgentRole {
4860 match role {
4861 WireRole::Explore => AgentRole::Explore,
4862 WireRole::Plan => AgentRole::Plan,
4863 WireRole::Implement => AgentRole::Implement,
4864 WireRole::Verify => AgentRole::Verify,
4865 WireRole::Custom => AgentRole::Custom,
4866 }
4867}
4868
4869fn core_isolation(isolation: WireIsolation) -> AgentIsolation {
4870 match isolation {
4871 WireIsolation::Shared => AgentIsolation::Shared,
4872 WireIsolation::ReadOnly => AgentIsolation::ReadOnly,
4873 WireIsolation::Worktree => AgentIsolation::Worktree,
4874 WireIsolation::Remote => AgentIsolation::Remote,
4875 }
4876}
4877
4878fn core_context_inheritance(inheritance: WireContextInheritance) -> ContextInheritance {
4879 match inheritance {
4880 WireContextInheritance::None => ContextInheritance::None,
4881 WireContextInheritance::SystemOnly => ContextInheritance::SystemOnly,
4882 WireContextInheritance::Full => ContextInheritance::Full,
4883 }
4884}
4885
4886fn parse_wire_role(label: &str) -> Option<WireRole> {
4890 match label {
4891 "explore" => Some(WireRole::Explore),
4892 "plan" => Some(WireRole::Plan),
4893 "implement" => Some(WireRole::Implement),
4894 "verify" => Some(WireRole::Verify),
4895 _ => None,
4896 }
4897}
4898
4899fn parse_wire_isolation(label: &str) -> Option<WireIsolation> {
4900 match label {
4901 "read_only" => Some(WireIsolation::ReadOnly),
4902 "worktree" => Some(WireIsolation::Worktree),
4903 "remote" => Some(WireIsolation::Remote),
4904 _ => None,
4905 }
4906}
4907
4908fn parse_wire_context_inheritance(label: &str) -> Option<WireContextInheritance> {
4909 match label {
4910 "none" => Some(WireContextInheritance::None),
4911 "system_only" => Some(WireContextInheritance::SystemOnly),
4912 "full" => Some(WireContextInheritance::Full),
4913 _ => None,
4914 }
4915}
4916
4917fn seed_initial_context(engine: &mut LoopStateMachine, initial: &InitialContext) {
4920 if !initial.messages.is_empty() {
4921 engine.preload_history(initial.messages.iter().map(logical_message).collect());
4922 }
4923 seed_knowledge(engine, &initial.knowledge);
4924}
4925
4926fn seed_knowledge(engine: &mut LoopStateMachine, entries: &[super::root::KnowledgeEntry]) {
4932 if entries.is_empty() {
4933 return;
4934 }
4935 let entries: Vec<crate::mm::PageInEntry> = entries
4936 .iter()
4937 .map(|entry| crate::mm::PageInEntry {
4938 content: entry.content.clone(),
4939 tokens: entry.tokens,
4940 source: None,
4941 key: entry.key.clone(),
4942 pinned: entry.pinned,
4943 })
4944 .collect();
4945 engine.apply_page_in(&entries);
4946}
4947
4948fn runtime_signal(
4969 signal: &LogicalSignal,
4970 accepted_at_ms: WireU64,
4971) -> crate::types::signal::RuntimeSignal {
4972 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
4973
4974 let source = match signal.source {
4975 Some(SignalSourceKind::Cron) => SignalSource::Cron,
4976 Some(SignalSourceKind::Gateway) => SignalSource::Gateway,
4977 Some(SignalSourceKind::Heartbeat) => SignalSource::Heartbeat,
4978 Some(SignalSourceKind::Custom) | None => SignalSource::Custom,
4979 };
4980 let urgency = match signal.urgency {
4981 Some(SignalUrgency::Low) => Urgency::Low,
4982 Some(SignalUrgency::High) => Urgency::High,
4983 Some(SignalUrgency::Critical) => Urgency::Critical,
4984 Some(SignalUrgency::Normal) | None => Urgency::Normal,
4985 };
4986 let mut runtime = RuntimeSignal::new(
4987 source,
4988 SignalType::Event,
4993 urgency,
4994 signal_summary(signal),
4995 )
4996 .with_id(signal.signal_id.as_str())
4997 .with_payload(signal.payload.get().clone())
4998 .with_timestamp(accepted_at_ms.get());
4999 if let Some(key) = &signal.dedupe_key {
5000 runtime = runtime.with_dedupe(key.as_str());
5001 }
5002 if let Some(after) = signal.escalate_after_ms {
5007 runtime = runtime.with_deadline(accepted_at_ms.get().saturating_add(after.get()));
5008 }
5009 runtime
5010}
5011
5012fn signal_summary(signal: &LogicalSignal) -> String {
5018 const SIGNAL_SUMMARY_MAX_BYTES: usize = 512;
5019 match signal.payload.get() {
5020 serde_json::Value::Null => signal.signal_id.as_str().to_string(),
5021 serde_json::Value::String(text) => {
5022 truncate_on_char_boundary(text, SIGNAL_SUMMARY_MAX_BYTES)
5023 }
5024 other => truncate_on_char_boundary(&other.to_string(), SIGNAL_SUMMARY_MAX_BYTES),
5025 }
5026}
5027
5028fn core_capability_kind(
5029 kind: super::root::CapabilityKind,
5030) -> crate::types::capability::CapabilityKind {
5031 use super::root::CapabilityKind as Wire;
5032 use crate::types::capability::CapabilityKind as Core;
5033 match kind {
5034 Wire::Tool => Core::Tool,
5035 Wire::Skill => Core::Skill,
5036 Wire::Memory => Core::Memory,
5037 Wire::Knowledge => Core::Knowledge,
5038 Wire::McpServer => Core::McpServer,
5039 Wire::Command => Core::Command,
5040 Wire::Agent => Core::Agent,
5041 }
5042}
5043
5044fn live_policy_label(patch: &super::command::LivePolicyPatch) -> &'static str {
5045 use super::command::LivePolicyPatch;
5046 match patch {
5047 LivePolicyPatch::ReplaceSignalPolicy(_) => "signal",
5048 LivePolicyPatch::ReplaceGovernancePolicy(_) => "governance",
5049 LivePolicyPatch::TightenResourceQuota(_) => "resource_quota",
5050 LivePolicyPatch::ReplaceRecoveryPolicy(_) => "recovery",
5051 }
5052}
5053
5054fn logical_message(message: &super::root::LogicalMessage) -> Message {
5055 Message {
5056 role: core_role_of(message.role),
5057 content: Content::Text(message.content.clone()),
5058 tool_calls: Vec::new(),
5059 token_count: message.tokens,
5060 }
5061}
5062
5063fn core_role_of(role: MessageRole) -> Role {
5064 match role {
5065 MessageRole::System => Role::System,
5066 MessageRole::User => Role::User,
5067 MessageRole::Assistant => Role::Assistant,
5068 MessageRole::Tool => Role::Tool,
5069 }
5070}
5071
5072fn wire_role_of(role: Role) -> MessageRole {
5073 match role {
5074 Role::System => MessageRole::System,
5075 Role::User => MessageRole::User,
5076 Role::Assistant => MessageRole::Assistant,
5077 Role::Tool => MessageRole::Tool,
5078 }
5079}
5080
5081fn rendered_context(context: &crate::context::renderer::RenderedContext) -> WireRenderedContext {
5082 WireRenderedContext {
5083 system_stable: context.system_stable.clone(),
5084 system_knowledge: context.system_knowledge.clone(),
5085 turns: context.turns.iter().map(provider_message).collect(),
5086 state_turn: context.state_turn.as_ref().map(provider_message),
5087 frozen_prefix_len: context.frozen_prefix_len.map(|len| len as u32),
5088 }
5089}
5090
5091fn provider_message(message: &Message) -> ProviderMessage {
5092 let (content, tool_call_id) = message_body_parts(message)
5093 .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
5094 .unwrap_or_default();
5095 ProviderMessage {
5096 role: wire_role_of(message.role),
5097 content,
5098 tool_calls: message
5099 .tool_calls
5100 .iter()
5101 .filter_map(|call| wire_tool_call(call).ok())
5102 .collect(),
5103 tool_call_id: tool_call_id.and_then(|call_id| super::scalar::CallId::new(call_id).ok()),
5104 tokens: message.token_count,
5105 }
5106}
5107
5108fn tool_schema(schema: &crate::types::message::ToolSchema) -> WireToolSchema {
5109 WireToolSchema {
5110 name: schema.name.to_string(),
5111 description: schema.description.clone(),
5112 parameters: super::scalar::BoundedJson::new(schema.parameters.clone())
5113 .unwrap_or_else(|_| Default::default()),
5114 }
5115}
5116
5117fn workflow_budget(budget: &crate::orchestration::workflow::WorkflowBudget) -> WireWorkflowBudget {
5118 WireWorkflowBudget {
5119 max_total_tokens: budget.tokens_max.map(WireU64::new),
5120 max_turns: None,
5121 max_concurrency: budget.max_concurrent_subagents.map(|max| max as u32),
5122 }
5123}
5124
5125fn sub_agent_result(completed: &ChildCompleted) -> SubAgentResult {
5126 let termination = match completed.result.status {
5127 ChildStatus::Completed => TerminationReason::Completed,
5128 ChildStatus::Failed => TerminationReason::Error,
5129 ChildStatus::Cancelled => TerminationReason::UserAbort,
5130 };
5131 SubAgentResult {
5132 agent_id: completed.task_id.as_str().into(),
5133 result: LoopResult {
5134 termination,
5135 final_message: completed
5136 .result
5137 .output
5138 .as_ref()
5139 .map(|text| Message::assistant(text.clone())),
5140 turns_used: completed
5141 .result
5142 .usage
5143 .as_ref()
5144 .and_then(|usage| usage.turns)
5145 .unwrap_or(0),
5146 total_tokens_used: completed
5147 .result
5148 .usage
5149 .as_ref()
5150 .and_then(|usage| usage.output_tokens)
5151 .map_or(0, WireU64::get),
5152 loop_continue: None,
5153 classify_branch: None,
5154 pace_decision: None,
5155 tournament_winner: None,
5156 },
5157 }
5158}
5159
5160fn agent_terminal(result: &LoopResult) -> KernelTerminal {
5167 let usage = UsageReport {
5168 input_tokens: WireU64::new(result.total_tokens_used),
5169 output_tokens: WireU64::ZERO,
5170 turns: result.turns_used,
5171 cached_input_tokens: None,
5172 };
5173 let termination = match result.termination {
5174 TerminationReason::Completed => WireTermination::Completed,
5175 TerminationReason::MaxTurns => WireTermination::MaxTurns,
5176 TerminationReason::TokenBudget => WireTermination::TokenBudget,
5177 TerminationReason::Timeout => WireTermination::Deadline,
5178 TerminationReason::ContextOverflow => WireTermination::ContextOverflow,
5179 TerminationReason::NoProgress => WireTermination::NoProgress,
5180 TerminationReason::MilestoneExceeded => WireTermination::MilestoneExceeded,
5181 TerminationReason::UserAbort => {
5182 return KernelTerminal::Cancelled(CancelledTerminal {
5183 reason: CancellationReason::User,
5184 usage,
5185 });
5186 }
5187 TerminationReason::Error => {
5188 return KernelTerminal::Failed(FailedTerminal {
5189 failure: KernelFailure {
5190 code: KernelFailureCode::InvariantViolated,
5191 message: "the agent loop ended in an error state".to_string(),
5192 },
5193 usage,
5194 });
5195 }
5196 };
5197 KernelTerminal::Agent(AgentTerminal {
5198 result: WireLoopResult {
5199 termination,
5200 final_message: result.final_message.as_ref().map(provider_message),
5201 turns_used: result.turns_used,
5202 pace_decision: result.pace_decision.as_ref().map(|decision| {
5203 super::terminal::PaceDecision {
5204 action: match decision.action {
5205 CorePaceAction::Continue => super::terminal::PaceAction::Continue,
5206 CorePaceAction::Sleep => super::terminal::PaceAction::Sleep,
5207 CorePaceAction::Stop => super::terminal::PaceAction::Stop,
5208 },
5209 delay_ms: decision.delay_ms.map(WireU64::new),
5210 reason: decision.reason.clone(),
5211 coerced_from: decision.coerced_from.clone(),
5212 }
5213 }),
5214 },
5215 usage,
5216 })
5217}
5218
5219fn publishes(disposition: &StepDisposition, tag: EffectKindTag) -> bool {
5220 disposition
5221 .effects()
5222 .iter()
5223 .any(|effect| effect.tag() == tag)
5224}
5225
5226fn loop_action_label(action: &LoopAction) -> &'static str {
5227 match action {
5228 LoopAction::CallLLM { .. } => "call_provider",
5229 LoopAction::ExecuteTools { .. } => "execute_tools",
5230 LoopAction::RequestApproval { .. } => "request_approval",
5231 LoopAction::SpawnWorkflow { .. } => "spawn_tasks",
5232 LoopAction::PreemptSubAgents { .. } => "preempt_tasks",
5233 LoopAction::PersistMemory { .. } => "persist_memory",
5234 LoopAction::QueryMemory { .. } => "query_memory",
5235 LoopAction::ArchivePageOut { .. } => "archive_page_out",
5236 LoopAction::EvaluateMilestone { .. } => "evaluate_milestone",
5237 LoopAction::Done { .. } => "terminal",
5238 LoopAction::AwaitingResume => "awaiting_resume",
5239 }
5240}
5241
5242fn syscall_ack(name: &str) -> &'static str {
5247 match name {
5248 "start_workflow" => {
5249 "workflow accepted: its ready nodes are scheduled; each result arrives as that node \
5250 completes"
5251 }
5252 "submit_workflow_nodes" => {
5253 "nodes appended to the running workflow; each result arrives as that node completes"
5254 }
5255 "skill" => "skill activated: its guidance and tools are in this turn's context",
5256 "update_plan" => "plan updated: the new state renders in [TASK STATE] from here on",
5257 crate::context::manager::MEMORY_TOOL_NAME => {
5258 "memory search issued: matching records are added to this conversation before your \
5259 next turn"
5260 }
5261 crate::context::manager::READ_RESULT_TOOL_NAME => "page-in requested",
5262 _ => "accepted",
5263 }
5264}
5265
5266fn core_provider_message(message: &ProviderMessage) -> Result<Message, KernelFault> {
5268 Ok(Message {
5269 role: core_role_of(message.role),
5270 content: Content::Text(message.content.clone()),
5271 tool_calls: message.tool_calls.iter().map(core_tool_call).collect(),
5272 token_count: message.tokens,
5273 })
5274}
5275
5276fn core_tool_call(call: &WireToolCall) -> crate::types::message::ToolCall {
5277 crate::types::message::ToolCall {
5278 id: call.call_id.as_str().into(),
5279 name: call.name.as_str().into(),
5280 arguments: call.arguments.get().clone(),
5281 }
5282}
5283
5284fn wire_tool_call(call: &crate::types::message::ToolCall) -> Result<WireToolCall, KernelFault> {
5285 Ok(WireToolCall {
5286 call_id: super::scalar::CallId::new(call.id.as_str()).map_err(malformed)?,
5287 name: call.name.to_string(),
5288 arguments: super::scalar::BoundedJson::new(call.arguments.clone())
5289 .unwrap_or_else(|_| Default::default()),
5290 })
5291}
5292
5293fn wire_approval_request(
5294 request: &crate::scheduler::state_machine::ApprovalRequest,
5295) -> Result<WireApprovalRequest, KernelFault> {
5296 Ok(WireApprovalRequest {
5297 call_id: super::scalar::CallId::new(request.call_id.as_str()).map_err(malformed)?,
5298 tool_name: request.tool.clone(),
5299 arguments: super::scalar::BoundedJson::new(request.arguments.clone())
5300 .unwrap_or_else(|_| Default::default()),
5301 reason: (!request.reason.is_empty()).then(|| request.reason.clone()),
5302 })
5303}
5304
5305fn core_tool_result(payload: &WireToolResultPayload) -> ToolResult {
5327 let disposition = payload.disposition();
5328 let is_error = payload.is_error();
5329 let error_kind = match disposition {
5330 ToolResultDisposition::Fatal => Some(ToolErrorKind::Fatal),
5331 ToolResultDisposition::Recoverable => is_error.then_some(ToolErrorKind::Recoverable),
5332 };
5333 match payload {
5334 WireToolResultPayload::Inline(inline) => ToolResult {
5335 call_id: inline.call_id.as_str().into(),
5336 output: Content::Text(inline.result.output.clone()),
5337 is_error,
5338 is_fatal: disposition.is_fatal(),
5339 error_kind,
5340 token_count: inline.result.tokens,
5341 },
5342 WireToolResultPayload::External(external) => ToolResult {
5343 call_id: external.call_id.as_str().into(),
5344 output: Content::Text(external.preview.clone()),
5345 is_error,
5346 is_fatal: disposition.is_fatal(),
5347 error_kind,
5348 token_count: None,
5349 },
5350 }
5351}
5352
5353fn check_payload_policy(
5369 payload: &WireToolResultPayload,
5370 policy: &super::config::ResolvedPayloadPolicy,
5371) -> Result<(), KernelFault> {
5372 let threshold = policy.inline_threshold_bytes as u64;
5373 match payload {
5374 WireToolResultPayload::Inline(inline) => {
5375 let size = inline.result.output.len() as u64;
5376 if size >= threshold {
5377 return Err(KernelFault::new(
5378 KernelFaultCode::ResourceLimitExceeded,
5379 format!(
5380 "tool result {} is {size} bytes and this operation's payload policy \
5381 externalises at {threshold}; the host persists the body and submits an \
5382 external result — the kernel does not spool on its behalf (§7.10)",
5383 inline.call_id
5384 ),
5385 ));
5386 }
5387 Ok(())
5388 }
5389 WireToolResultPayload::External(external) => {
5390 if !is_verifiable_digest(external.digest.as_str()) {
5391 return Err(KernelFault::new(
5392 KernelFaultCode::MalformedEnvelope,
5393 format!(
5394 "external tool result {} carries digest {}, which this kernel cannot \
5395 verify; a paged-in body is checked by recomputing {}:<64 hex> over it",
5396 external.call_id,
5397 external.digest,
5398 super::record::DIGEST_ALGORITHM
5399 ),
5400 ));
5401 }
5402 let size = external.original_size.get();
5403 if size < threshold {
5404 return Err(KernelFault::new(
5405 KernelFaultCode::MalformedEnvelope,
5406 format!(
5407 "external tool result {} declares {size} bytes but this operation's \
5408 payload policy inlines below {threshold}; the threshold is the single \
5409 arbiter of which arm a result takes (§7.10)",
5410 external.call_id
5411 ),
5412 ));
5413 }
5414 let preview = external.preview.len() as u64;
5415 if preview > policy.preview_bytes as u64 {
5416 return Err(KernelFault::new(
5417 KernelFaultCode::ResourceLimitExceeded,
5418 format!(
5419 "external tool result {} carries a {preview}-byte preview and this \
5420 operation keeps {} bytes resident",
5421 external.call_id, policy.preview_bytes
5422 ),
5423 ));
5424 }
5425 Ok(())
5426 }
5427 }
5428}
5429
5430fn is_verifiable_digest(digest: &str) -> bool {
5432 let Some(hex) = digest.strip_prefix(super::record::DIGEST_ALGORITHM) else {
5433 return false;
5434 };
5435 let Some(hex) = hex.strip_prefix(':') else {
5436 return false;
5437 };
5438 hex.len() == 64
5439 && hex
5440 .bytes()
5441 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
5442}
5443
5444fn core_milestone_result(
5445 result: &super::effect::MilestoneCheckResult,
5446) -> crate::types::milestone::MilestoneCheckResult {
5447 crate::types::milestone::MilestoneCheckResult {
5448 phase_id: result.phase_id.clone(),
5449 passed: result.passed,
5450 reason: (!result.passed).then(|| {
5451 if result.failed_criteria.is_empty() {
5452 result.notes.clone()
5453 } else {
5454 format!("unmet criteria: {}", result.failed_criteria.join("; "))
5455 }
5456 }),
5457 }
5458}
5459
5460fn core_milestone_contract(
5469 contract: &super::config::VerificationContract,
5470 config: &ResolvedOperationConfig,
5471) -> crate::types::milestone::MilestoneContract {
5472 use crate::types::capability::{CapabilityDescriptor, CapabilityKind as CoreCapabilityKind};
5473 use crate::types::milestone::{MilestoneContract, MilestonePhase};
5474
5475 let mut cascade = MilestoneContract::new();
5476 for phase in &contract.phases {
5477 let unlocks = phase
5478 .unlocks
5479 .iter()
5480 .map(|id| {
5481 if let Some(tool) = config.tool_catalog.iter().find(|tool| &tool.name == id) {
5482 CapabilityDescriptor::tool(core_tool_schema(tool))
5483 } else if let Some(skill) = config.skill_catalog.iter().find(|s| &s.name == id) {
5484 CapabilityDescriptor::skill(core_skill(skill))
5485 } else {
5486 CapabilityDescriptor::marker(
5487 CoreCapabilityKind::Tool,
5488 id.as_str(),
5489 String::new(),
5490 )
5491 }
5492 })
5493 .collect();
5494 cascade = cascade.phase(MilestonePhase {
5495 unlocks,
5496 ..MilestonePhase::new(phase.phase_id.clone())
5497 });
5498 }
5499 cascade
5500}
5501
5502fn core_memory_kind(kind: WireMemoryKind) -> crate::mm::memory::MemoryKind {
5503 match kind {
5504 WireMemoryKind::User => crate::mm::memory::MemoryKind::User,
5505 WireMemoryKind::Feedback => crate::mm::memory::MemoryKind::Feedback,
5506 WireMemoryKind::Project => crate::mm::memory::MemoryKind::Project,
5507 WireMemoryKind::Reference => crate::mm::memory::MemoryKind::Reference,
5508 }
5509}
5510
5511fn wire_memory_kind_label(kind: WireMemoryKind) -> &'static str {
5512 core_memory_kind(kind).label()
5513}
5514
5515fn binding_scope(binding_id: &MemoryBindingId) -> crate::mm::memory::MemoryScope {
5520 crate::mm::memory::MemoryScope::new(String::new(), binding_id.as_str().to_string())
5521}
5522
5523fn host_failure_text(failure: &HostEffectFailure) -> String {
5526 if failure.message.is_empty() {
5527 failure.kind.as_str().to_string()
5528 } else {
5529 format!("{}: {}", failure.kind.as_str(), failure.message)
5530 }
5531}
5532
5533fn unowned_resolution(effect_id: &EffectId, what: &str) -> KernelFault {
5537 KernelFault::new(
5538 KernelFaultCode::RecordCorrupted,
5539 format!(
5540 "effect {effect_id} resolves a {what} this runtime never authored; the driver's ledger \
5541 no longer describes the journal — rebuild from the records"
5542 ),
5543 )
5544}
5545
5546fn truncate_on_char_boundary(text: &str, max_bytes: usize) -> String {
5547 if text.len() <= max_bytes {
5548 return text.to_string();
5549 }
5550 let mut end = max_bytes;
5551 while end > 0 && !text.is_char_boundary(end) {
5552 end -= 1;
5553 }
5554 text[..end].to_string()
5555}
5556
5557fn build_engine(config: &ResolvedOperationConfig) -> LoopStateMachine {
5565 let execution = &config.execution_policy;
5566 let mut engine = LoopStateMachine::new(SchedulerBudget {
5567 max_tokens: execution.max_context_tokens,
5568 max_turns: execution.max_turns,
5569 max_total_tokens: execution.max_total_tokens.get(),
5570 max_wall_ms: execution.max_wall_ms.map(WireU64::get),
5571 });
5572 if let Some(grant) = config.budget_grant.clone() {
5573 engine.set_budget_grant(grant);
5574 }
5575 let scheduler_policy = config.scheduler_policy;
5576 engine.set_scheduler_policy(crate::scheduler::policy::SchedulerPolicyConfig {
5577 version: crate::scheduler::policy::SCHEDULER_POLICY_VERSION,
5578 critical_path_weight: i64::from(scheduler_policy.critical_path_weight),
5579 fanout_weight: i64::from(scheduler_policy.fanout_weight),
5580 age_weight: i64::from(scheduler_policy.age_weight),
5581 token_cost_weight: i64::from(scheduler_policy.token_cost_weight),
5582 });
5583
5584 engine.set_criteria_gate(execution.criteria_gate_enabled);
5585 engine.set_repeat_fuse(crate::governance::repeat_fuse::RepeatFuseConfig {
5586 enabled: execution.repeat_fuse.enabled,
5587 deny_after: execution.repeat_fuse.deny_after,
5588 terminate_after: execution.repeat_fuse.terminate_after,
5589 });
5590 engine.set_entropy_watch(crate::scheduler::entropy::EntropyWatchConfig {
5591 enabled: execution.entropy_watch.enabled,
5592 threshold: f64::from(execution.entropy_watch.threshold_ppm.get()) / 1_000_000.0,
5593 hysteresis: f64::from(execution.entropy_watch.hysteresis_ppm.get()) / 1_000_000.0,
5594 cooldown_turns: execution.entropy_watch.cooldown_turns,
5595 notify_model: execution.entropy_watch.notify_model,
5596 });
5597 install_live_policies(&mut engine, config);
5598 engine.set_dispatch_gate_exposed(matches!(
5599 config.feature_policy.tool_dispatch_gate,
5600 super::config::ToolDispatchGate::Exposed
5601 ));
5602 engine
5603 .ctx
5604 .set_memory_enabled(config.feature_policy.memory_enabled);
5605 engine
5606 .ctx
5607 .set_knowledge_enabled(config.feature_policy.knowledge_enabled);
5608 engine
5609 .ctx
5610 .set_plan_tool_enabled(config.feature_policy.plan_tool_enabled);
5611 engine
5614 .ctx
5615 .set_available_skills(config.skill_catalog.iter().map(core_skill).collect());
5616 engine.ctx.set_stable_core_tools(
5617 config
5618 .feature_policy
5619 .stable_core_tool_ids
5620 .iter()
5621 .map(|id| id.as_str().into()),
5622 );
5623 engine.ctx.config.knowledge_budget_ratio =
5624 config.context_policy.knowledge_budget_ppm.as_ratio();
5625 engine.ctx.config.collapse_assistant_narration =
5626 config.context_policy.collapse_old_assistant_narration;
5627 engine.tools = config.tool_catalog.iter().map(core_tool_schema).collect();
5628 engine
5629}
5630
5631fn install_live_policies(engine: &mut LoopStateMachine, config: &ResolvedOperationConfig) {
5640 if let Some(quota) = core_quota(&config.resource_quota) {
5644 engine.set_resource_quota(quota);
5645 }
5646 if let Some(pipeline) = core_governance(&config.governance_policy) {
5650 engine.set_governance(pipeline);
5651 }
5652 engine.set_signal_policy(
5653 config.signal_policy.queue_max as usize,
5654 config.signal_policy.ttl_ms.map(WireU64::get),
5655 config.signal_policy.deadline_escalation,
5656 );
5657 engine.set_recovery_limits(
5663 config.recovery_policy.provider_recovery_attempts,
5664 config.recovery_policy.output_recovery_attempts,
5665 );
5666}
5667
5668fn core_quota(
5672 quota: &super::config::ResourceQuota,
5673) -> Option<crate::governance::quota::ResourceQuota> {
5674 if quota == &super::config::ResourceQuota::default() {
5675 return None;
5676 }
5677 Some(crate::governance::quota::ResourceQuota {
5678 max_concurrent_subagents: quota.max_concurrent_subagents,
5679 max_total_subagents: quota.max_total_subagents,
5680 max_spawn_depth: quota.max_spawn_depth,
5681 memory_writes_per_window: quota
5682 .memory_writes_per_window
5683 .as_ref()
5684 .map(|window| (window.max_events, window.window_ms.get())),
5685 max_workflow_nodes: quota.max_workflow_nodes.map(|max| max as usize),
5686 })
5687}
5688
5689fn core_governance(
5693 policy: &super::config::ResolvedGovernancePolicy,
5694) -> Option<crate::governance::pipeline::GovernancePipeline> {
5695 use super::command::{ParamConstraint as WireConstraint, PolicyAction};
5696 use crate::governance::constraint::{ConstraintRule, ParamConstraint as CoreConstraint};
5697 use crate::governance::permission::PermissionRule;
5698 use crate::governance::rate_limit::RateLimit;
5699
5700 if policy.default_action == PolicyAction::Allow
5701 && policy.rules.is_empty()
5702 && policy.vetoed_tools.is_empty()
5703 && policy.rate_limits.is_empty()
5704 && policy.constraints.is_empty()
5705 {
5706 return None;
5707 }
5708 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(core_policy_action(
5709 policy.default_action,
5710 ));
5711 for rule in &policy.rules {
5712 pipeline.permission.add_rule(PermissionRule {
5713 tool_pattern: rule.tool_pattern.as_str().into(),
5714 action: core_policy_action(rule.action),
5715 });
5716 }
5717 for tool in &policy.vetoed_tools {
5718 pipeline.veto.block_tool(tool.clone());
5719 }
5720 for limit in &policy.rate_limits {
5721 pipeline.rate_limiter.set_limit(
5722 limit.tool.clone(),
5723 RateLimit {
5724 max_calls: limit.max_calls,
5725 window_ms: limit.window_ms.get(),
5726 },
5727 );
5728 }
5729 for constraint in &policy.constraints {
5730 let rule = match constraint {
5731 WireConstraint::Required(_) => ConstraintRule::Required,
5732 WireConstraint::Enum(spec) => ConstraintRule::Enum(spec.values.clone()),
5733 WireConstraint::Range(spec) => ConstraintRule::Range {
5736 min: spec.min_micros.map(|micros| micros as f64 / 1_000_000.0),
5737 max: spec.max_micros.map(|micros| micros as f64 / 1_000_000.0),
5738 },
5739 };
5740 pipeline.constraints.add(CoreConstraint {
5741 tool_name: constraint.tool().to_string(),
5742 param_path: constraint.param_path().to_string(),
5743 rule,
5744 });
5745 }
5746 Some(pipeline)
5747}
5748
5749fn core_policy_action(
5750 action: super::command::PolicyAction,
5751) -> crate::governance::permission::PermissionAction {
5752 use crate::governance::permission::PermissionAction;
5753 match action {
5754 super::command::PolicyAction::Allow => PermissionAction::Allow,
5755 super::command::PolicyAction::Deny => PermissionAction::Deny,
5756 super::command::PolicyAction::AskUser => PermissionAction::AskUser,
5757 }
5758}
5759
5760fn core_skill(skill: &super::config::SkillMetadata) -> crate::types::skill::SkillMetadata {
5761 crate::types::skill::SkillMetadata {
5762 name: skill.name.as_str().into(),
5763 description: skill.description.clone(),
5764 when_to_use: skill.when_to_use.clone(),
5765 allowed_tools: skill
5766 .allowed_tools
5767 .iter()
5768 .map(|tool| tool.as_str().into())
5769 .collect(),
5770 effort: skill.effort,
5771 estimated_tokens: skill.estimated_tokens.unwrap_or(0),
5772 }
5773}
5774
5775fn core_tool_schema(schema: &WireToolSchema) -> crate::types::message::ToolSchema {
5776 crate::types::message::ToolSchema {
5777 name: schema.name.as_str().into(),
5778 description: schema.description.clone(),
5779 parameters: schema.parameters.get().clone(),
5780 }
5781}
5782
5783fn malformed(error: super::scalar::WireScalarError) -> KernelFault {
5784 KernelFault::new(KernelFaultCode::MalformedEnvelope, error.message)
5785}
5786
5787#[cfg(test)]
5788mod tests {
5789 use std::fs;
5790 use std::path::PathBuf;
5791
5792 use serde_json::{Value, json};
5793
5794 use super::*;
5795 use crate::runtime::kernel::wire::checkpoint::{
5796 CanonicalInput, CheckpointCandidate, CheckpointDraft, KernelCheckpoint,
5797 };
5798 use crate::runtime::kernel::wire::config::ConfigDefaults;
5799 use crate::runtime::kernel::wire::config::TailBounds;
5800 use crate::runtime::kernel::wire::config::{
5801 EntropyWatchPolicy, ExecutionPolicy, HostEffectSupport,
5802 MilestonePhase as WireMilestonePhase, OperationConfig,
5803 VerificationContract as WireVerificationContract,
5804 };
5805 use crate::runtime::kernel::wire::effect::{
5806 EffectSucceeded, TaskLaunchOutcome, TaskLaunchStarted, TaskLaunchStatus,
5807 TasksSpawnedSuccess,
5808 };
5809 use crate::runtime::kernel::wire::envelope::{
5810 ConfigureOperation, DeliverExternalEvent, KernelInput, ResolveEffect, StartOperation,
5811 WireEnvelope,
5812 };
5813 use crate::runtime::kernel::wire::event::{ChildResult, DeliverSignal, LogicalSignal};
5814 use crate::runtime::kernel::wire::fault::PrepareToken;
5815 use crate::runtime::kernel::wire::record::{
5816 KernelRecord, RecordPreparation, verify_record_chain,
5817 };
5818 use crate::runtime::kernel::wire::restore::{
5819 RestoreCost, RestoredOperation, restore_operation,
5820 };
5821 use crate::runtime::kernel::wire::root::{
5822 RootAgentEntry, RootWorkflowEntry, WorkflowNode as WireNode,
5823 };
5824 use crate::runtime::kernel::wire::scalar::{
5825 AttemptId as WireAttemptId, DeliveryId, InputId, Ppm, SignalId,
5826 };
5827 use crate::runtime::kernel::wire::transaction::{
5828 CheckpointBoundary, CommittedTransition, InMemoryRecordIndex, KernelTransaction,
5829 TailPressure,
5830 };
5831 use crate::scheduler::tcb::TaskLifecycle;
5832
5833 const OPERATION: &str = "op-driver-1";
5834
5835 struct Runtime {
5844 tx: KernelTransaction<PlannedStep, InMemoryRecordIndex>,
5845 driver: CanonicalOperationDriver,
5846 journal: Vec<KernelRecord>,
5847 last_observations: Vec<KernelObservation>,
5848 restore_cost: Option<RestoreCost>,
5850 }
5851
5852 impl Runtime {
5853 fn new() -> Self {
5854 Self {
5855 tx: KernelTransaction::new(ConfigDefaults::default(), InMemoryRecordIndex::new()),
5856 driver: CanonicalOperationDriver::new(),
5857 journal: Vec::new(),
5858 last_observations: Vec::new(),
5859 restore_cost: None,
5860 }
5861 }
5862
5863 fn prepare(&mut self, envelope: &WireEnvelope) -> RecordPreparation<PlannedStep> {
5864 let Self { tx, driver, .. } = self;
5865 tx.prepare(envelope, |context| driver.plan(context))
5866 }
5867
5868 fn append_and_commit(
5869 &mut self,
5870 preparation: RecordPreparation<PlannedStep>,
5871 ) -> CommittedTransition<PlannedStep> {
5872 let token: PrepareToken = preparation
5873 .token()
5874 .unwrap_or_else(|| {
5875 panic!("expected a prepared step, got {:?}", preparation.fault())
5876 })
5877 .clone();
5878 let head = preparation.record().unwrap().record_digest().clone();
5879 let committed = self.tx.commit(&token, &head).expect("commit must succeed");
5880 self.journal.push(committed.record.clone());
5881 self.last_observations = committed.step.observations.clone();
5882 committed
5883 }
5884
5885 fn submit(&mut self, envelope: &WireEnvelope) -> CommittedTransition<PlannedStep> {
5886 let preparation = self.prepare(envelope);
5887 let committed = self.append_and_commit(preparation);
5888 self.driver
5889 .note_committed(committed.step_seq)
5890 .expect("the driver folds the step it planned");
5891 committed
5892 }
5893
5894 fn submit_planned<F>(
5898 &mut self,
5899 envelope: &WireEnvelope,
5900 plan: F,
5901 ) -> CommittedTransition<PlannedStep>
5902 where
5903 F: FnOnce(
5904 &mut CanonicalOperationDriver,
5905 &PlanContext<'_>,
5906 ) -> Result<PlannedStep, KernelFault>,
5907 {
5908 let preparation = {
5909 let Self { tx, driver, .. } = self;
5910 tx.prepare(envelope, |context| plan(driver, context))
5911 };
5912 self.append_and_commit(preparation)
5913 }
5914
5915 fn reject(&mut self, envelope: &WireEnvelope) -> KernelFault {
5916 let preparation = self.prepare(envelope);
5917 preparation
5918 .fault()
5919 .unwrap_or_else(|| panic!("expected a rejection, got a prepared step"))
5920 .clone()
5921 }
5922
5923 fn restore(&self, checkpoint: &KernelCheckpoint) -> Runtime {
5927 let after: Vec<KernelRecord> = self
5928 .journal
5929 .iter()
5930 .filter(|record| record.step_seq().get() > checkpoint.through_step_seq().get())
5931 .cloned()
5932 .collect();
5933 Self::restore_with(Some(checkpoint), &after)
5934 }
5935
5936 fn restore_with(
5937 checkpoint: Option<&KernelCheckpoint>,
5938 records: &[KernelRecord],
5939 ) -> Runtime {
5940 let RestoredOperation {
5941 transaction,
5942 driver,
5943 cost,
5944 } = restore_operation(
5945 checkpoint,
5946 records,
5947 ConfigDefaults::default(),
5948 InMemoryRecordIndex::from_records(records),
5949 )
5950 .expect("the restore ladder runs to completion");
5951 Runtime {
5952 tx: transaction,
5953 driver,
5954 journal: Vec::new(),
5955 last_observations: Vec::new(),
5956 restore_cost: Some(cost),
5957 }
5958 }
5959
5960 fn journal_from(&self, checkpoint: &KernelCheckpoint) -> Vec<KernelRecord> {
5962 self.journal
5963 .iter()
5964 .filter(|record| record.step_seq().get() > checkpoint.through_step_seq().get())
5965 .cloned()
5966 .collect()
5967 }
5968
5969 fn pending_effect_kinds(&self) -> Vec<EffectKindTag> {
5970 self.tx.pending_effects().map(|e| e.tag()).collect()
5971 }
5972
5973 fn observations(&self) -> &[KernelObservation] {
5974 &self.last_observations
5975 }
5976
5977 fn checkpoint(&self) -> CheckpointCandidate {
5980 self.tx
5981 .checkpoint_candidate(self.driver.project_logical_state())
5982 .expect("a configured operation has a logical state to checkpoint")
5983 }
5984 }
5985
5986 fn operation() -> OperationId {
5991 OperationId::new(OPERATION).unwrap()
5992 }
5993
5994 fn envelope(id: &str, observed_at_ms: u64, input: KernelInput) -> WireEnvelope {
5995 WireEnvelope::new(
5996 operation(),
5997 InputId::new(id).unwrap(),
5998 WireU64::new(observed_at_ms),
5999 input,
6000 )
6001 }
6002
6003 fn boot_config(supported: impl IntoIterator<Item = EffectKindTag>) -> OperationConfig {
6004 OperationConfig {
6005 execution_policy: Some(ExecutionPolicy {
6006 max_turns: Some(12),
6007 ..ExecutionPolicy::default()
6008 }),
6009 host_effect_support: HostEffectSupport::new(supported),
6010 ..OperationConfig::default()
6011 }
6012 }
6013
6014 fn configure() -> WireEnvelope {
6015 configure_supporting([EffectKindTag::CallProvider, EffectKindTag::SpawnTasks])
6016 }
6017
6018 fn configure_supporting(supported: impl IntoIterator<Item = EffectKindTag>) -> WireEnvelope {
6019 envelope(
6020 "in-configure",
6021 1_700_000_000_000,
6022 KernelInput::ConfigureOperation(ConfigureOperation {
6023 config: boot_config(supported),
6024 }),
6025 )
6026 }
6027
6028 fn agent_start(id: &str, observed_at_ms: u64) -> WireEnvelope {
6029 envelope(
6030 id,
6031 observed_at_ms,
6032 KernelInput::StartOperation(StartOperation {
6033 entry: RootEntry::Agent(RootAgentEntry {
6034 task: LogicalTask::new("write the research brief"),
6035 run_spec: None,
6036 }),
6037 initial_context: InitialContext::default(),
6038 }),
6039 )
6040 }
6041
6042 fn wire_node(node_id: &str, goal: &str, depends_on: &[&str]) -> WireNode {
6043 WireNode {
6044 node_id: NodeId::new(node_id).unwrap(),
6045 task: LogicalTask::new(goal),
6046 depends_on: depends_on
6047 .iter()
6048 .map(|id| NodeId::new(*id).unwrap())
6049 .collect(),
6050 run_spec: None,
6051 }
6052 }
6053
6054 fn two_node_spec() -> WireSpec {
6055 WireSpec {
6056 name: "brief".to_string(),
6057 nodes: vec![
6058 wire_node("collect", "collect the sources", &[]),
6059 wire_node("write", "write the brief", &["collect"]),
6060 ],
6061 }
6062 }
6063
6064 fn workflow_start(id: &str, observed_at_ms: u64, spec: WireSpec) -> WireEnvelope {
6065 envelope(
6066 id,
6067 observed_at_ms,
6068 KernelInput::StartOperation(StartOperation {
6069 entry: RootEntry::Workflow(RootWorkflowEntry { spec }),
6070 initial_context: InitialContext::default(),
6071 }),
6072 )
6073 }
6074
6075 fn spawned(
6076 id: &str,
6077 observed_at_ms: u64,
6078 effect_id: &EffectId,
6079 tasks: &[&str],
6080 ) -> WireEnvelope {
6081 envelope(
6082 id,
6083 observed_at_ms,
6084 KernelInput::ResolveEffect(ResolveEffect {
6085 effect_id: effect_id.clone(),
6086 outcome: EffectOutcome::Succeeded(EffectSucceeded {
6087 result: EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
6088 attempts: tasks
6089 .iter()
6090 .map(|task| TaskLaunchOutcome {
6091 task_id: TaskId::new(*task).unwrap(),
6092 attempt_id: WireAttemptId::new(format!("{task}:attempt:1"))
6093 .unwrap(),
6094 outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
6095 })
6096 .collect(),
6097 }),
6098 }),
6099 }),
6100 )
6101 }
6102
6103 fn child_done(id: &str, observed_at_ms: u64, task: &str, output: &str) -> WireEnvelope {
6104 envelope(
6105 id,
6106 observed_at_ms,
6107 KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6108 event: ExternalEvent::ChildCompleted(ChildCompleted {
6109 task_id: TaskId::new(task).unwrap(),
6110 attempt_id: WireAttemptId::new(format!("{task}:attempt:1")).unwrap(),
6111 result: ChildResult {
6112 status: ChildStatus::Completed,
6113 output: Some(output.to_string()),
6114 ..ChildResult::default()
6115 },
6116 parent_requests: Vec::new(),
6117 }),
6118 }),
6119 )
6120 }
6121
6122 fn syscall_carrier(id: &str, observed_at_ms: u64) -> WireEnvelope {
6130 envelope(
6131 id,
6132 observed_at_ms,
6133 KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6134 event: ExternalEvent::DeliverSignal(DeliverSignal {
6135 delivery_id: DeliveryId::new(format!("delivery-{id}")).unwrap(),
6136 attempt: 1,
6137 signal: LogicalSignal::new(SignalId::new(format!("sig-{id}")).unwrap()),
6138 }),
6139 }),
6140 )
6141 }
6142
6143 fn effect_id(step_seq: WireU64) -> EffectId {
6144 EffectId::new(format!("{OPERATION}:step:{step_seq}:effect:0")).unwrap()
6145 }
6146
6147 fn effect_id_at(step_seq: WireU64, index: u32) -> EffectId {
6148 EffectId::new(format!("{OPERATION}:step:{step_seq}:effect:{index}")).unwrap()
6149 }
6150
6151 fn syscall_tool_catalog() -> Vec<WireToolSchema> {
6156 SYSCALL_TOOL_NAMES
6157 .iter()
6158 .chain(std::iter::once(&"search"))
6159 .map(|name| WireToolSchema {
6160 name: (*name).to_string(),
6161 description: String::new(),
6162 parameters: Default::default(),
6163 })
6164 .collect()
6165 }
6166
6167 fn syscall_config() -> WireEnvelope {
6171 syscall_config_with(|_| {})
6172 }
6173
6174 fn syscall_config_with(edit: impl FnOnce(&mut OperationConfig)) -> WireEnvelope {
6177 use crate::runtime::kernel::wire::config::{
6178 MemoryPolicy, ResourceQuota, SkillMetadata as WireSkill,
6179 };
6180 use crate::runtime::kernel::wire::effect::{MemoryAccessBinding, MemoryCapabilities};
6181 use crate::runtime::kernel::wire::scalar::MemoryBindingId;
6182
6183 let mut config = {
6184 OperationConfig {
6185 execution_policy: Some(ExecutionPolicy {
6186 max_turns: Some(12),
6187 ..ExecutionPolicy::default()
6188 }),
6189 host_effect_support: HostEffectSupport::new([
6190 EffectKindTag::CallProvider,
6191 EffectKindTag::ExecuteTools,
6192 EffectKindTag::LoadPayload,
6193 EffectKindTag::SpawnTasks,
6194 EffectKindTag::PreemptTasks,
6195 EffectKindTag::PersistMemory,
6196 EffectKindTag::QueryMemory,
6197 ]),
6198 tool_catalog: syscall_tool_catalog(),
6199 skill_catalog: vec![WireSkill {
6200 name: "debug".to_string(),
6201 description: "debug helper".to_string(),
6202 when_to_use: None,
6203 allowed_tools: Vec::new(),
6204 effort: None,
6205 estimated_tokens: None,
6206 }],
6207 memory_access: Some(MemoryAccessBinding {
6208 binding_id: MemoryBindingId::new("mem-binding-1").unwrap(),
6209 capabilities: MemoryCapabilities {
6210 read: true,
6211 write: true,
6212 },
6213 }),
6214 memory_policy: Some(MemoryPolicy {
6215 retrieval_top_k: Some(4),
6216 ..MemoryPolicy::default()
6217 }),
6218 resource_quota: Some(ResourceQuota {
6219 max_workflow_nodes: Some(3),
6220 ..ResourceQuota::default()
6221 }),
6222 ..OperationConfig::default()
6223 }
6224 };
6225 edit(&mut config);
6226 envelope(
6227 "in-configure",
6228 1_700_000_000_000,
6229 KernelInput::ConfigureOperation(ConfigureOperation { config }),
6230 )
6231 }
6232
6233 fn tool_call(call_id: &str, name: &str, arguments: Value) -> WireToolCall {
6234 WireToolCall {
6235 call_id: super::super::scalar::CallId::new(call_id).unwrap(),
6236 name: name.to_string(),
6237 arguments: super::super::scalar::BoundedJson::new(arguments).unwrap(),
6238 }
6239 }
6240
6241 fn provider_result(
6244 id: &str,
6245 observed_at_ms: u64,
6246 effect: &EffectId,
6247 calls: Vec<WireToolCall>,
6248 ) -> WireEnvelope {
6249 envelope(
6250 id,
6251 observed_at_ms,
6252 KernelInput::ResolveEffect(ResolveEffect {
6253 effect_id: effect.clone(),
6254 outcome: EffectOutcome::Succeeded(EffectSucceeded {
6255 result: EffectSuccess::Provider(super::super::effect::ProviderSuccess {
6256 outcome: super::super::effect::ProviderOutcome::Completed(
6257 super::super::effect::ProviderCompleted {
6258 message: ProviderMessage {
6259 role: MessageRole::Assistant,
6260 content: String::new(),
6261 tool_calls: calls,
6262 tool_call_id: None,
6263 tokens: None,
6264 },
6265 observed_input_tokens: None,
6266 observed_output_tokens: None,
6267 stop_reason: None,
6268 },
6269 ),
6270 }),
6271 }),
6272 }),
6273 )
6274 }
6275
6276 fn child_done_with(
6277 id: &str,
6278 observed_at_ms: u64,
6279 task: &str,
6280 attempt: &str,
6281 requests: Vec<SyscallRequest>,
6282 ) -> WireEnvelope {
6283 envelope(
6284 id,
6285 observed_at_ms,
6286 KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6287 event: ExternalEvent::ChildCompleted(ChildCompleted {
6288 task_id: TaskId::new(task).unwrap(),
6289 attempt_id: WireAttemptId::new(attempt).unwrap(),
6290 result: ChildResult {
6291 status: ChildStatus::Completed,
6292 output: Some("done".to_string()),
6293 ..ChildResult::default()
6294 },
6295 parent_requests: requests,
6296 }),
6297 }),
6298 )
6299 }
6300
6301 fn node_args(nodes: &[WireNode]) -> Value {
6302 json!({ "nodes": serde_json::to_value(nodes).unwrap() })
6303 }
6304
6305 fn rejections(runtime: &Runtime) -> Vec<(String, Option<String>, String)> {
6307 runtime
6308 .observations()
6309 .iter()
6310 .filter_map(|observation| match observation {
6311 KernelObservation::ControlRequestRejected {
6312 operation,
6313 subject,
6314 reason,
6315 ..
6316 } => Some((operation.clone(), subject.clone(), reason.clone())),
6317 _ => None,
6318 })
6319 .collect()
6320 }
6321
6322 fn sole_effect(committed: &CommittedTransition<PlannedStep>) -> &KernelEffect {
6323 let effects = committed.published_effects();
6324 assert_eq!(effects.len(), 1, "expected exactly one published effect");
6325 &effects[0]
6326 }
6327
6328 #[test]
6333 fn the_first_accepted_input_must_be_the_configuration() {
6334 let mut runtime = Runtime::new();
6335
6336 let fault = runtime.reject(&agent_start("in-early-start", 1_700_000_000_100));
6338 assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6339 assert_eq!(runtime.tx.head(), None, "a rejection moves nothing");
6340 assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Created);
6341 assert!(
6342 runtime.driver.engine().is_none(),
6343 "no semantic kernel exists"
6344 );
6345
6346 let genesis = runtime.submit(&configure());
6347 assert_eq!(genesis.step_seq, WireU64::ZERO);
6348 assert_eq!(
6349 genesis.record.previous_record_digest(),
6350 None,
6351 "the genesis record has no predecessor (§8.1)"
6352 );
6353 assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Configured);
6354 assert!(runtime.driver.engine().is_some());
6355 assert_eq!(
6356 runtime.driver.root_kind(),
6357 None,
6358 "configuring is not starting"
6359 );
6360
6361 let stored = genesis.record.normalized_input().unwrap();
6363 let resolved = stored
6364 .resolved_config()
6365 .expect("genesis carries the config");
6366 assert_eq!(resolved.execution_policy.max_turns, 12);
6367 assert_eq!(
6368 resolved.execution_policy.max_context_tokens,
6369 ConfigDefaults::default()
6370 .baseline
6371 .execution_policy
6372 .max_context_tokens,
6373 "every default this operation runs on is frozen in its first record"
6374 );
6375 }
6376
6377 #[test]
6382 fn an_agent_root_start_is_one_atomic_input_that_issues_the_model_turn() {
6383 let mut runtime = Runtime::new();
6384 runtime.submit(&configure());
6385 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6386
6387 assert_eq!(started.step.root_kind, Some(RootKind::Agent));
6389 assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
6390 assert_eq!(
6391 runtime.driver.focus(),
6392 Some(&ExecutionFocus::agent_turn(root_task_id())),
6393 );
6394 assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Running);
6395
6396 let effect = sole_effect(&started);
6398 assert_eq!(effect.tag(), EffectKindTag::CallProvider);
6399 assert_eq!(
6400 effect.effect_id,
6401 effect_id(started.step_seq),
6402 "the effect id is minted by the kernel from the step it belongs to"
6403 );
6404 assert_eq!(
6405 effect.causation_input_id.as_str(),
6406 "in-start",
6407 "every effect names the accepted input that produced it"
6408 );
6409 assert_eq!(
6410 runtime.pending_effect_kinds(),
6411 vec![EffectKindTag::CallProvider]
6412 );
6413
6414 let EffectKind::CallProvider(call) = &effect.effect else {
6416 panic!("expected a provider call");
6417 };
6418 let rendered = format!(
6419 "{}{}",
6420 call.context.system_stable, call.context.system_knowledge
6421 );
6422 let state_turn = call
6423 .context
6424 .state_turn
6425 .as_ref()
6426 .map(|turn| turn.content.clone())
6427 .unwrap_or_default();
6428 assert!(
6429 rendered.contains("research brief") || state_turn.contains("research brief"),
6430 "the root task's goal must be in the rendered context, not merely stored"
6431 );
6432 assert!(
6433 !call.context.turns.is_empty(),
6434 "the start seeded a first turn"
6435 );
6436 }
6437
6438 #[test]
6439 fn a_second_root_start_is_refused_with_zero_mutation() {
6440 let mut runtime = Runtime::new();
6441 runtime.submit(&configure());
6442 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6443
6444 let before = (
6445 runtime.tx.head(),
6446 runtime.tx.lifecycle(),
6447 runtime.pending_effect_kinds(),
6448 runtime.driver.root_kind(),
6449 runtime.driver.focus().cloned(),
6450 );
6451
6452 let fault = runtime.reject(&agent_start("in-start-again", 1_700_000_002_000));
6453 assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6454
6455 let after = (
6456 runtime.tx.head(),
6457 runtime.tx.lifecycle(),
6458 runtime.pending_effect_kinds(),
6459 runtime.driver.root_kind(),
6460 runtime.driver.focus().cloned(),
6461 );
6462 assert_eq!(before, after, "a refused root start moves nothing");
6463 assert_eq!(runtime.journal.len(), 2, "no third record exists");
6464 assert_eq!(started.step.root_kind, Some(RootKind::Agent));
6465 }
6466
6467 #[test]
6468 fn a_workflow_root_start_after_an_agent_root_cannot_re_root_the_operation() {
6469 let mut runtime = Runtime::new();
6470 runtime.submit(&configure());
6471 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6472
6473 let fault = runtime.reject(&workflow_start(
6474 "in-reroot",
6475 1_700_000_002_000,
6476 two_node_spec(),
6477 ));
6478 assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6479 assert_eq!(
6480 runtime.driver.root_kind(),
6481 Some(RootKind::Agent),
6482 "the root kind is immutable for the operation's lifetime (§6.1.5)"
6483 );
6484 }
6485
6486 #[test]
6490 fn a_workflow_root_is_refused_when_the_host_cannot_spawn_tasks() {
6491 let mut runtime = Runtime::new();
6492 runtime.submit(&configure_supporting([EffectKindTag::CallProvider]));
6493 let fault = runtime.reject(&workflow_start(
6494 "in-start",
6495 1_700_000_001_000,
6496 two_node_spec(),
6497 ));
6498 assert_eq!(fault.code, KernelFaultCode::UnsupportedEffect);
6499 assert_eq!(runtime.driver.root_kind(), None);
6500 assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Configured);
6501 assert_eq!(runtime.journal.len(), 1, "only the genesis record exists");
6502 }
6503
6504 #[test]
6509 fn a_workflow_root_start_spawns_tasks_and_never_calls_the_provider() {
6510 let mut runtime = Runtime::new();
6511 runtime.submit(&configure());
6512 let started = runtime.submit(&workflow_start(
6513 "in-start",
6514 1_700_000_001_000,
6515 two_node_spec(),
6516 ));
6517
6518 assert_eq!(started.step.root_kind, Some(RootKind::Workflow));
6519 let effect = sole_effect(&started);
6520 assert_eq!(
6521 effect.tag(),
6522 EffectKindTag::SpawnTasks,
6523 "a workflow root's first effect is a task spawn (§10.1)"
6524 );
6525 assert_eq!(
6526 runtime.pending_effect_kinds(),
6527 vec![EffectKindTag::SpawnTasks],
6528 "no provider effect is published, so none can be overwritten by a workflow load"
6529 );
6530
6531 let EffectKind::SpawnTasks(spawn) = &effect.effect else {
6533 panic!("expected a task spawn");
6534 };
6535 assert_eq!(spawn.tasks.len(), 1, "only `collect` is ready");
6536 assert_eq!(spawn.tasks[0].node_id.as_str(), "collect");
6537 assert_eq!(spawn.tasks[0].task_id.as_str(), "wf-node0");
6538 assert!(
6539 !spawn.tasks[0].launch_token.as_str().is_empty(),
6540 "the launch token exists as a committed fact before the host launches anything"
6541 );
6542
6543 assert_eq!(
6545 runtime.driver.focus(),
6546 Some(&ExecutionFocus::workflow_controller(
6547 runtime.driver.workflow_id().unwrap().clone(),
6548 None
6549 ))
6550 );
6551 assert!(
6552 !runtime.driver.focus().unwrap().is_nested_in_agent(),
6553 "the root workflow is not nested in an agent"
6554 );
6555 }
6556
6557 #[test]
6558 fn a_workflow_launch_preserves_logical_context_inheritance() {
6559 let mut spec = two_node_spec();
6560 spec.nodes[0].run_spec = Some(LogicalAgentSpec {
6561 context_inheritance: Some(WireContextInheritance::SystemOnly),
6562 ..LogicalAgentSpec::new("collect the sources")
6563 });
6564
6565 let mut runtime = Runtime::new();
6566 runtime.submit(&configure());
6567 let started = runtime.submit(&workflow_start("in-start", 1_700_000_001_000, spec));
6568
6569 let EffectKind::SpawnTasks(spawn) = &sole_effect(&started).effect else {
6570 panic!("expected a task spawn");
6571 };
6572 assert_eq!(
6573 spawn.tasks[0].spec.context_inheritance,
6574 Some(WireContextInheritance::SystemOnly),
6575 );
6576 }
6577
6578 #[test]
6579 fn a_workflow_root_with_no_nodes_has_no_first_effect_and_is_refused() {
6580 let mut runtime = Runtime::new();
6581 runtime.submit(&configure());
6582 let fault = runtime.reject(&workflow_start(
6583 "in-start",
6584 1_700_000_001_000,
6585 WireSpec::default(),
6586 ));
6587 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
6588 assert_eq!(runtime.driver.root_kind(), None);
6589 }
6590
6591 #[test]
6592 fn a_workflow_spec_whose_dependency_names_no_declared_node_is_refused() {
6593 let mut runtime = Runtime::new();
6594 runtime.submit(&configure());
6595 let spec = WireSpec {
6596 name: "broken".to_string(),
6597 nodes: vec![wire_node("write", "write", &["collect"])],
6598 };
6599 let fault = runtime.reject(&workflow_start("in-start", 1_700_000_001_000, spec));
6600 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
6601 assert_eq!(runtime.driver.root_kind(), None);
6602 assert_eq!(runtime.journal.len(), 1, "only the genesis record exists");
6603 }
6604
6605 fn drive_workflow_root_to_terminal() -> Runtime {
6612 let mut runtime = Runtime::new();
6613 runtime.submit(&configure());
6614 let started = runtime.submit(&workflow_start(
6615 "in-start",
6616 1_700_000_001_000,
6617 two_node_spec(),
6618 ));
6619
6620 runtime.submit(&spawned(
6621 "in-ack-1",
6622 1_700_000_002_000,
6623 &effect_id(started.step_seq),
6624 &["wf-node0"],
6625 ));
6626 let advanced = runtime.submit(&child_done(
6627 "in-done-1",
6628 1_700_000_003_000,
6629 "wf-node0",
6630 "sources collected",
6631 ));
6632 let second = sole_effect(&advanced);
6633 assert_eq!(second.tag(), EffectKindTag::SpawnTasks);
6634 let EffectKind::SpawnTasks(spawn) = &second.effect else {
6635 panic!("expected a task spawn");
6636 };
6637 assert_eq!(spawn.tasks[0].node_id.as_str(), "write");
6638
6639 runtime.submit(&spawned(
6640 "in-ack-2",
6641 1_700_000_004_000,
6642 &effect_id(advanced.step_seq),
6643 &["wf-node1"],
6644 ));
6645 runtime.submit(&child_done(
6646 "in-done-2",
6647 1_700_000_005_000,
6648 "wf-node1",
6649 "brief written",
6650 ));
6651 runtime
6652 }
6653
6654 #[test]
6655 fn a_root_workflow_completion_commits_the_workflow_terminal_itself() {
6656 let runtime = drive_workflow_root_to_terminal();
6657
6658 let terminal = runtime.tx.terminal().expect("the run terminated");
6659 let KernelTerminal::Workflow(workflow) = terminal else {
6660 panic!("a workflow root terminates with a workflow terminal, got {terminal:?}");
6661 };
6662 assert_eq!(workflow.outcome.status, WorkflowStatus::Completed);
6663 assert_eq!(
6664 workflow
6665 .outcome
6666 .completed_nodes
6667 .iter()
6668 .map(|node| node.as_str())
6669 .collect::<Vec<_>>(),
6670 vec!["collect", "write"],
6671 "the terminal names the wire node ids the host declared"
6672 );
6673 assert!(workflow.outcome.failed_nodes.is_empty());
6674 assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Completed);
6675 assert_eq!(
6676 runtime.pending_effect_kinds(),
6677 Vec::<EffectKindTag>::new(),
6678 "a root workflow issues no provider call after it completes"
6679 );
6680 }
6681
6682 #[test]
6683 fn a_completion_naming_an_attempt_the_kernel_never_minted_is_refused() {
6684 let mut runtime = Runtime::new();
6685 runtime.submit(&configure());
6686 let started = runtime.submit(&workflow_start(
6687 "in-start",
6688 1_700_000_001_000,
6689 two_node_spec(),
6690 ));
6691 runtime.submit(&spawned(
6692 "in-ack-1",
6693 1_700_000_002_000,
6694 &effect_id(started.step_seq),
6695 &["wf-node0"],
6696 ));
6697
6698 let forged = envelope(
6699 "in-forged",
6700 1_700_000_003_000,
6701 KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6702 event: ExternalEvent::ChildCompleted(ChildCompleted {
6703 task_id: TaskId::new("wf-node0").unwrap(),
6704 attempt_id: WireAttemptId::new("wf-node0:attempt:7").unwrap(),
6705 result: ChildResult::default(),
6706 parent_requests: Vec::new(),
6707 }),
6708 }),
6709 );
6710 let before = runtime.pending_effect_kinds();
6711 let fault = runtime.reject(&forged);
6712 assert_eq!(
6713 fault.code,
6714 KernelFaultCode::InvalidAuthority,
6715 "a host does not mint or rewrite child identity (§10.4)"
6716 );
6717 assert_eq!(runtime.pending_effect_kinds(), before);
6718 assert!(runtime.tx.terminal().is_none());
6719 }
6720
6721 #[test]
6722 fn a_terminated_root_workflow_refuses_every_later_state_changing_input() {
6723 let mut runtime = drive_workflow_root_to_terminal();
6724 let fault = runtime.reject(&child_done(
6725 "in-done-late",
6726 1_700_000_006_000,
6727 "wf-node1",
6728 "again",
6729 ));
6730 assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6731 }
6732
6733 fn provider_settled(
6741 driver: &mut CanonicalOperationDriver,
6742 _context: &PlanContext<'_>,
6743 ) -> Result<PlannedStep, KernelFault> {
6744 Ok(PlannedStep::quiet(
6745 driver.root_kind(),
6746 driver.focus().cloned(),
6747 ))
6748 }
6749
6750 fn agent_root_with_nested_workflow() -> (Runtime, TaskId) {
6751 let mut runtime = Runtime::new();
6752 runtime.submit(&configure());
6753 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6754
6755 let settle = envelope(
6757 "in-provider",
6758 1_700_000_002_000,
6759 KernelInput::ResolveEffect(ResolveEffect {
6760 effect_id: effect_id(started.step_seq),
6761 outcome: EffectOutcome::Succeeded(EffectSucceeded {
6762 result: EffectSuccess::Provider(super::super::effect::ProviderSuccess {
6763 outcome: super::super::effect::ProviderOutcome::ContextOverflow(
6764 super::super::effect::ProviderContextOverflow::default(),
6765 ),
6766 }),
6767 }),
6768 }),
6769 );
6770 runtime.submit_planned(&settle, provider_settled);
6771
6772 let spec = two_node_spec();
6773 let carrier = syscall_carrier("in-submit-workflow", 1_700_000_003_000);
6774 let entered = runtime.submit_planned(&carrier, |driver, context| {
6775 driver.begin_nested_workflow(context, &spec)
6776 });
6777 runtime
6778 .driver
6779 .note_committed(entered.step_seq)
6780 .expect("the nested start folds like any other planned step");
6781 assert_eq!(sole_effect(&entered).tag(), EffectKindTag::SpawnTasks);
6782 (runtime, root_task_id())
6783 }
6784
6785 #[test]
6789 fn starting_a_workflow_never_overwrites_a_live_provider_effect() {
6790 let mut runtime = Runtime::new();
6791 runtime.submit(&configure());
6792 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6793 let provider_effect = sole_effect(&started).effect_id.clone();
6794
6795 let spec = two_node_spec();
6796 let carrier = syscall_carrier("in-submit-workflow", 1_700_000_002_000);
6797 let entered = runtime.submit_planned(&carrier, |driver, context| {
6798 driver.begin_nested_workflow(context, &spec)
6799 });
6800 runtime.driver.note_committed(entered.step_seq).unwrap();
6801
6802 let mut pending = runtime.pending_effect_kinds();
6803 pending.sort();
6804 assert_eq!(
6805 pending,
6806 vec![EffectKindTag::CallProvider, EffectKindTag::SpawnTasks],
6807 "the provider call is still outstanding; the spawn is a second registration"
6808 );
6809 assert!(
6810 runtime
6811 .tx
6812 .pending_effects()
6813 .any(|effect| effect.effect_id == provider_effect),
6814 "the provider effect kept its identity, so the host can still resolve it"
6815 );
6816 }
6817
6818 #[test]
6819 fn an_agent_authored_workflow_moves_the_focus_but_never_the_root_kind() {
6820 let (runtime, parent) = agent_root_with_nested_workflow();
6821
6822 assert_eq!(
6823 runtime.driver.root_kind(),
6824 Some(RootKind::Agent),
6825 "a syscall never re-roots an operation (§10.2)"
6826 );
6827 let focus = runtime.driver.focus().expect("a focus exists");
6828 assert!(
6829 focus.is_nested_in_agent(),
6830 "the focus records the parent agent task it must restore"
6831 );
6832 assert_eq!(
6833 focus,
6834 &ExecutionFocus::workflow_controller(
6835 runtime.driver.workflow_id().unwrap().clone(),
6836 Some(parent),
6837 )
6838 );
6839 }
6840
6841 #[test]
6842 fn a_second_workflow_inside_a_workflow_focus_is_an_authority_refusal_with_no_spawn() {
6843 let (mut runtime, _) = agent_root_with_nested_workflow();
6844
6845 let before = (
6846 runtime.tx.head(),
6847 runtime.pending_effect_kinds(),
6848 runtime.driver.root_kind(),
6849 runtime.driver.focus().cloned(),
6850 );
6851
6852 let spec = two_node_spec();
6853 let carrier = syscall_carrier("in-submit-again", 1_700_000_004_000);
6854 let preparation = {
6855 let Runtime { tx, driver, .. } = &mut runtime;
6856 tx.prepare(&carrier, |context| {
6857 driver.begin_nested_workflow(context, &spec)
6858 })
6859 };
6860 let fault = preparation.fault().expect("expected a refusal").clone();
6861 assert_eq!(
6862 fault.code,
6863 KernelFaultCode::InvalidAuthority,
6864 "workflows do not stack — depth is at most 1 (§7.4)"
6865 );
6866
6867 let after = (
6868 runtime.tx.head(),
6869 runtime.pending_effect_kinds(),
6870 runtime.driver.root_kind(),
6871 runtime.driver.focus().cloned(),
6872 );
6873 assert_eq!(before, after, "the refusal produced no derived action");
6874 }
6875
6876 #[test]
6877 fn a_nested_workflow_completion_restores_the_parent_agent_and_resumes_its_turn() {
6878 let (mut runtime, parent) = agent_root_with_nested_workflow();
6879 let spawn_step = runtime.journal.last().unwrap().step_seq();
6880
6881 runtime.submit(&spawned(
6882 "in-ack-1",
6883 1_700_000_004_000,
6884 &effect_id(spawn_step),
6885 &["wf-node0"],
6886 ));
6887 let advanced = runtime.submit(&child_done(
6888 "in-done-1",
6889 1_700_000_005_000,
6890 "wf-node0",
6891 "sources collected",
6892 ));
6893 runtime.submit(&spawned(
6894 "in-ack-2",
6895 1_700_000_006_000,
6896 &effect_id(advanced.step_seq),
6897 &["wf-node1"],
6898 ));
6899 let finished = runtime.submit(&child_done(
6900 "in-done-2",
6901 1_700_000_007_000,
6902 "wf-node1",
6903 "brief written",
6904 ));
6905
6906 assert!(
6907 runtime.tx.terminal().is_none(),
6908 "a nested workflow's completion is not the operation's terminal (§6.1.7)"
6909 );
6910 assert_eq!(
6911 runtime.driver.focus(),
6912 Some(&ExecutionFocus::agent_turn(parent)),
6913 "the focus returns to the agent turn it left"
6914 );
6915 assert_eq!(
6916 sole_effect(&finished).tag(),
6917 EffectKindTag::CallProvider,
6918 "the parent agent's turn resumes with a provider call"
6919 );
6920 assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
6921 }
6922
6923 #[test]
6924 fn a_workflow_root_admits_no_nested_workflow_at_all() {
6925 let mut runtime = Runtime::new();
6926 runtime.submit(&configure());
6927 runtime.submit(&workflow_start(
6928 "in-start",
6929 1_700_000_001_000,
6930 two_node_spec(),
6931 ));
6932
6933 let spec = two_node_spec();
6934 let carrier = syscall_carrier("in-submit", 1_700_000_002_000);
6935 let preparation = {
6936 let Runtime { tx, driver, .. } = &mut runtime;
6937 tx.prepare(&carrier, |context| {
6938 driver.begin_nested_workflow(context, &spec)
6939 })
6940 };
6941 assert_eq!(
6942 preparation.fault().unwrap().code,
6943 KernelFaultCode::InvalidAuthority
6944 );
6945 }
6946
6947 #[test]
6948 fn a_workflow_roots_focus_never_moves_while_its_dag_runs() {
6949 let mut runtime = Runtime::new();
6950 runtime.submit(&configure());
6951 let started = runtime.submit(&workflow_start(
6952 "in-start",
6953 1_700_000_001_000,
6954 two_node_spec(),
6955 ));
6956 let focus = runtime.driver.focus().cloned();
6957
6958 runtime.submit(&spawned(
6959 "in-ack-1",
6960 1_700_000_002_000,
6961 &effect_id(started.step_seq),
6962 &["wf-node0"],
6963 ));
6964 assert_eq!(
6965 runtime.driver.focus().cloned(),
6966 focus,
6967 "an ack moves nothing"
6968 );
6969
6970 let advanced = runtime.submit(&child_done(
6971 "in-done-1",
6972 1_700_000_003_000,
6973 "wf-node0",
6974 "done",
6975 ));
6976 assert_eq!(
6977 runtime.driver.focus().cloned(),
6978 focus,
6979 "a DAG node's agent execution is a child attempt, not a focus change"
6980 );
6981 assert_eq!(sole_effect(&advanced).tag(), EffectKindTag::SpawnTasks);
6982 }
6983
6984 fn agent_awaiting_provider() -> (Runtime, EffectId) {
6991 let mut runtime = Runtime::new();
6992 runtime.submit(&syscall_config());
6993 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6994 let effect = sole_effect(&started);
6995 assert_eq!(effect.tag(), EffectKindTag::CallProvider);
6996 let EffectKind::CallProvider(call) = &effect.effect else {
6997 panic!("expected a provider call");
6998 };
6999 assert!(
7000 call.tools.iter().any(|tool| tool.name == "start_workflow"),
7001 "the turn must actually expose the meta-tool the model is about to call"
7002 );
7003 (runtime, effect.effect_id.clone())
7004 }
7005
7006 #[test]
7007 fn a_provider_tool_call_derives_its_caller_and_enters_p1() {
7008 let (mut runtime, provider) = agent_awaiting_provider();
7009
7010 let spec = serde_json::to_value(two_node_spec()).unwrap();
7011 let entered = runtime.submit(&provider_result(
7012 "in-authored",
7013 1_700_000_002_000,
7014 &provider,
7015 vec![tool_call("call-1", "start_workflow", spec)],
7016 ));
7017
7018 let effect = sole_effect(&entered);
7020 assert_eq!(effect.tag(), EffectKindTag::SpawnTasks);
7021 assert_eq!(
7022 runtime.driver.root_kind(),
7023 Some(RootKind::Agent),
7024 "a syscall never re-roots an operation (§10.2)"
7025 );
7026 assert!(
7027 runtime.driver.focus().unwrap().is_nested_in_agent(),
7028 "the focus moved to the workflow controller, under the agent turn it suspended"
7029 );
7030
7031 let stored = entered.record.normalized_input().unwrap();
7033 let json = serde_json::to_value(&stored).unwrap();
7034 let text = json.to_string();
7035 for forbidden in ["submitter_agent_id", "actor_id", "parent_session_id"] {
7036 assert!(
7037 !text.contains(forbidden),
7038 "the canonical input still carries {forbidden}"
7039 );
7040 }
7041 }
7042
7043 #[test]
7044 fn a_tool_the_turn_never_exposed_has_no_caller_to_derive() {
7045 let (mut runtime, provider) = agent_awaiting_provider();
7046
7047 let committed = runtime.submit(&provider_result(
7051 "in-forged",
7052 1_700_000_002_000,
7053 &provider,
7054 vec![tool_call(
7055 "call-1",
7056 "escalate_privileges",
7057 json!({"nodes": []}),
7058 )],
7059 ));
7060 assert_eq!(
7061 committed
7062 .published_effects()
7063 .iter()
7064 .map(|effect| effect.tag())
7065 .collect::<Vec<_>>(),
7066 vec![EffectKindTag::CallProvider],
7067 "a phantom tool is never dispatched; the turn is answered and re-asked"
7068 );
7069
7070 let next = effect_id(committed.step_seq);
7073 runtime
7074 .driver
7075 .provider_calls
7076 .get_mut(&next)
7077 .expect("the driver recorded the turn it published")
7078 .exposed_tools
7079 .clear();
7080 let before = (
7081 runtime.tx.head(),
7082 runtime.pending_effect_kinds(),
7083 runtime.driver.focus().cloned(),
7084 );
7085 let fault = runtime
7086 .driver
7087 .derive_provider_syscalls(&next, &[tool_call("call-2", "start_workflow", json!({}))])
7088 .expect_err("a tool the turn never exposed has no causation");
7089 assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7090 assert!(fault.message.contains("exposed no tool"));
7091
7092 let after = (
7093 runtime.tx.head(),
7094 runtime.pending_effect_kinds(),
7095 runtime.driver.focus().cloned(),
7096 );
7097 assert_eq!(before, after, "a forged causation moves nothing");
7098 }
7099
7100 #[test]
7101 fn a_provider_effect_this_kernel_never_published_has_no_causation() {
7102 let (runtime, _) = agent_awaiting_provider();
7103 let unknown = EffectId::new("op-driver-1:step:99:effect:0").unwrap();
7104 let fault = runtime
7105 .driver
7106 .derive_provider_syscalls(
7107 &unknown,
7108 &[tool_call("call-1", "start_workflow", json!({}))],
7109 )
7110 .expect_err("an unpublished effect names no turn");
7111 assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7112 assert!(fault.message.contains("not a provider call"));
7113 }
7114
7115 #[test]
7116 fn a_call_id_that_already_produced_a_syscall_cannot_produce_a_second() {
7117 let (mut runtime, provider) = agent_awaiting_provider();
7118
7119 let fault = runtime.reject(&provider_result(
7121 "in-double",
7122 1_700_000_002_000,
7123 &provider,
7124 vec![
7125 tool_call("call-1", "skill", json!({"name": "debug"})),
7126 tool_call("call-1", "skill", json!({"name": "debug"})),
7127 ],
7128 ));
7129 assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7130 assert!(fault.message.contains("consumed once"));
7131
7132 runtime.submit(&provider_result(
7134 "in-skill",
7135 1_700_000_002_000,
7136 &provider,
7137 vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
7138 ));
7139 assert!(
7140 runtime.driver.consumed_calls.contains("call-1"),
7141 "the spent causation is remembered, so a redelivery under a fresh input id buys nothing"
7142 );
7143 assert!(
7144 !runtime.driver.provider_calls.contains_key(&provider),
7145 "a resolved provider call is no longer a surface anything can be attributed to"
7146 );
7147 }
7148
7149 #[test]
7150 fn a_skill_the_operation_never_declared_cannot_be_activated() {
7151 let (mut runtime, provider) = agent_awaiting_provider();
7152 runtime.submit(&provider_result(
7153 "in-skill",
7154 1_700_000_002_000,
7155 &provider,
7156 vec![tool_call(
7157 "call-1",
7158 "skill",
7159 json!({"name": "not-declared"}),
7160 )],
7161 ));
7162 let rejected = rejections(&runtime);
7163 assert_eq!(rejected.len(), 1, "the refusal is an audit fact");
7164 assert_eq!(rejected[0].0, "skill");
7165 assert_eq!(
7166 rejected[0].1.as_deref(),
7167 Some(ROOT_TASK_ID),
7168 "the audit fact names the caller the kernel derived, not one a host supplied"
7169 );
7170 assert!(rejected[0].2.contains("declares no skill"));
7171 assert_eq!(
7172 runtime.pending_effect_kinds(),
7173 vec![EffectKindTag::CallProvider],
7174 "a rejected capability mutation publishes no effect of its own; the turn still \
7175 continues (the §5k syscall-only continuation)"
7176 );
7177 }
7178
7179 #[test]
7180 fn an_append_with_no_graph_to_append_to_is_an_audit_fact_not_a_derived_action() {
7181 let (mut runtime, provider) = agent_awaiting_provider();
7182 runtime.submit(&provider_result(
7183 "in-append",
7184 1_700_000_002_000,
7185 &provider,
7186 vec![tool_call(
7187 "call-1",
7188 "submit_workflow_nodes",
7189 node_args(&[wire_node("stray", "stray", &[])]),
7190 )],
7191 ));
7192
7193 let rejected = rejections(&runtime);
7194 assert_eq!(rejected.len(), 1);
7195 assert_eq!(rejected[0].0, "submit_workflow_nodes");
7196 assert_eq!(
7197 rejected[0].1.as_deref(),
7198 Some(ROOT_TASK_ID),
7199 "a provider-tool causation names the task whose turn issued the call"
7200 );
7201 assert!(rejected[0].2.contains("no workflow is in flight"));
7202 assert_eq!(
7203 runtime.pending_effect_kinds(),
7204 vec![EffectKindTag::CallProvider],
7205 "a refused append spawns nothing; the turn continues with the next provider call"
7206 );
7207 }
7208
7209 fn workflow_root_awaiting_first_child() -> Runtime {
7215 let mut runtime = Runtime::new();
7216 runtime.submit(&syscall_config());
7217 let started = runtime.submit(&workflow_start(
7218 "in-start",
7219 1_700_000_001_000,
7220 two_node_spec(),
7221 ));
7222 runtime.submit(&spawned(
7223 "in-ack-1",
7224 1_700_000_002_000,
7225 &effect_id(started.step_seq),
7226 &["wf-node0"],
7227 ));
7228 runtime
7229 }
7230
7231 #[test]
7232 fn parent_requests_are_adjudicated_independently_and_never_undo_the_completion() {
7233 let mut runtime = workflow_root_awaiting_first_child();
7234
7235 let good = SyscallRequest::AppendWorkflowNodes(
7236 super::super::syscall::AppendWorkflowNodesRequest {
7237 nodes: vec![wire_node("verify", "verify the sources", &[])],
7238 },
7239 );
7240 let bad = SyscallRequest::AppendWorkflowNodes(
7242 super::super::syscall::AppendWorkflowNodesRequest {
7243 nodes: vec![wire_node("orphan", "orphan", &["nowhere"])],
7244 },
7245 );
7246 let another_good = SyscallRequest::UpdateTask(super::super::syscall::UpdateTaskRequest {
7247 update: WireTaskUpdate {
7248 progress: Some("sources collected".to_string()),
7249 ..WireTaskUpdate::default()
7250 },
7251 });
7252
7253 let advanced = runtime.submit(&child_done_with(
7254 "in-done-1",
7255 1_700_000_003_000,
7256 "wf-node0",
7257 "wf-node0:attempt:1",
7258 vec![good, bad, another_good],
7259 ));
7260
7261 assert_eq!(
7263 runtime.tx.lifecycle(),
7264 OperationLifecycle::Running,
7265 "a denied parent request does not undo the child's execution (GAP-4)"
7266 );
7267 let effect = sole_effect(&advanced);
7268 assert_eq!(effect.tag(), EffectKindTag::SpawnTasks);
7269 let EffectKind::SpawnTasks(spawn) = &effect.effect else {
7270 panic!("expected a task spawn");
7271 };
7272 let launched: Vec<&str> = spawn
7273 .tasks
7274 .iter()
7275 .map(|task| task.node_id.as_str())
7276 .collect();
7277 assert!(
7278 launched.contains(&"write") && launched.contains(&"verify"),
7279 "the admitted append reached the next ready batch alongside the original DAG, got \
7280 {launched:?}"
7281 );
7282 assert!(
7283 !launched.contains(&"orphan"),
7284 "the refused append produced no derived action"
7285 );
7286
7287 let rejected = rejections(&runtime);
7289 assert_eq!(rejected.len(), 1, "each request is adjudicated on its own");
7290 assert_eq!(rejected[0].0, "submit_workflow_nodes");
7291 assert_eq!(
7292 rejected[0].1.as_deref(),
7293 Some("wf-node0"),
7294 "the refusal names the child attempt it was derived from"
7295 );
7296 assert!(
7297 runtime
7298 .driver
7299 .engine()
7300 .unwrap()
7301 .ctx
7302 .partitions
7303 .task_state
7304 .progress
7305 .contains("sources collected"),
7306 "a sibling's refusal does not stop the requests after it"
7307 );
7308 }
7309
7310 #[test]
7311 fn an_append_beyond_the_workflow_node_quota_is_denied_without_touching_the_graph() {
7312 let mut runtime = workflow_root_awaiting_first_child();
7313 let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7314
7315 let oversized = SyscallRequest::AppendWorkflowNodes(
7317 super::super::syscall::AppendWorkflowNodesRequest {
7318 nodes: vec![
7319 wire_node("a", "a", &[]),
7320 wire_node("b", "b", &[]),
7321 wire_node("c", "c", &[]),
7322 ],
7323 },
7324 );
7325 runtime.submit(&child_done_with(
7326 "in-done-1",
7327 1_700_000_003_000,
7328 "wf-node0",
7329 "wf-node0:attempt:1",
7330 vec![oversized],
7331 ));
7332
7333 let rejected = rejections(&runtime);
7334 assert!(
7335 rejected.iter().any(|(operation, subject, reason)| operation
7336 == "submit_workflow_nodes"
7337 && subject.as_deref() == Some("wf-node0")
7338 && reason.contains("would grow workflow")),
7339 "the resource gate refused the growth, got {rejected:?}"
7340 );
7341 assert_eq!(
7342 runtime.driver.engine().unwrap().workflow_node_count(),
7343 nodes_before,
7344 "a denied append leaves the graph exactly as it was"
7345 );
7346 }
7347
7348 #[test]
7349 fn a_quarantined_task_cannot_widen_its_authority_through_a_syscall() {
7350 let mut runtime = workflow_root_awaiting_first_child();
7351 assert!(
7352 runtime
7353 .driver
7354 .engine_mut()
7355 .unwrap()
7356 .quarantine_task_for_test("wf-node0"),
7357 "the node must exist to be quarantined"
7358 );
7359 let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7360
7361 let append = SyscallRequest::AppendWorkflowNodes(
7362 super::super::syscall::AppendWorkflowNodesRequest {
7363 nodes: vec![wire_node("escalate", "escalate", &[])],
7364 },
7365 );
7366 let activate = SyscallRequest::ActivateSkill(super::super::syscall::ActivateSkillRequest {
7367 name: "debug".to_string(),
7368 lease_turns: None,
7369 });
7370 let remember =
7371 SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7372 proposal: super::super::syscall::MemoryWriteProposal {
7373 name: "escalation".to_string(),
7374 kind: WireMemoryKind::Project,
7375 content: "trust me".to_string(),
7376 description: String::new(),
7377 evidence_refs: Vec::new(),
7378 },
7379 });
7380
7381 runtime.submit(&child_done_with(
7382 "in-done-1",
7383 1_700_000_003_000,
7384 "wf-node0",
7385 "wf-node0:attempt:1",
7386 vec![append, activate, remember],
7387 ));
7388
7389 let quarantine_denials = rejections(&runtime);
7390 let families: Vec<&str> = quarantine_denials
7391 .iter()
7392 .filter(|(_, _, reason)| reason.starts_with("quarantine:"))
7393 .map(|(operation, _, _)| operation.as_str())
7394 .collect();
7395 assert_eq!(
7396 families,
7397 vec!["workflow", "capability", "memory"],
7398 "every privileged family is refused for a quarantined caller"
7399 );
7400 assert_eq!(
7401 runtime.driver.engine().unwrap().workflow_node_count(),
7402 nodes_before,
7403 "no node was appended"
7404 );
7405 assert!(
7406 !runtime
7407 .driver
7408 .engine()
7409 .unwrap()
7410 .ctx
7411 .active_skills
7412 .contains_key("debug"),
7413 "no skill was activated"
7414 );
7415 assert_eq!(
7416 runtime.pending_effect_kinds(),
7417 vec![EffectKindTag::SpawnTasks],
7418 "no memory effect was published; only the DAG's own next batch"
7419 );
7420 }
7421
7422 #[test]
7423 fn a_child_request_cannot_forge_a_second_root_workflow() {
7424 let mut runtime = workflow_root_awaiting_first_child();
7425 let workflow_before = runtime.driver.workflow_id().cloned();
7426 let focus_before = runtime.driver.focus().cloned();
7427
7428 let authored =
7429 SyscallRequest::SubmitWorkflow(super::super::syscall::SubmitWorkflowRequest {
7430 spec: WireSpec {
7431 name: "usurper".to_string(),
7432 nodes: vec![wire_node("usurp", "take over", &[])],
7433 },
7434 });
7435 runtime.submit(&child_done_with(
7436 "in-done-1",
7437 1_700_000_003_000,
7438 "wf-node0",
7439 "wf-node0:attempt:1",
7440 vec![authored],
7441 ));
7442
7443 assert_eq!(
7444 runtime.driver.root_kind(),
7445 Some(RootKind::Workflow),
7446 "the root kind is immutable (§6.1.5)"
7447 );
7448 assert_eq!(
7449 runtime.driver.workflow_id().cloned(),
7450 workflow_before,
7451 "an authored spec flattens into the running DAG; it never becomes a second root"
7452 );
7453 assert_eq!(
7454 runtime.driver.focus().cloned(),
7455 focus_before,
7456 "a workflow root's focus never moves (§7.4)"
7457 );
7458 assert!(
7459 rejections(&runtime).is_empty(),
7460 "flattening is the admitted path, not a refusal"
7461 );
7462 }
7463
7464 #[test]
7469 fn a_child_moves_pending_launch_then_starting_then_running_on_the_acknowledgement() {
7470 let mut runtime = Runtime::new();
7471 runtime.submit(&syscall_config());
7472
7473 let preparation = runtime.prepare(&workflow_start(
7475 "in-start",
7476 1_700_000_001_000,
7477 two_node_spec(),
7478 ));
7479 assert_eq!(
7480 runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7481 Some(TaskLifecycle::Starting),
7482 "the launch effect is planned, so the task left PendingLaunch and awaits the host"
7483 );
7484 let started = runtime.append_and_commit(preparation);
7485 runtime.driver.note_committed(started.step_seq).unwrap();
7486 assert_eq!(
7487 runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7488 Some(TaskLifecycle::Starting),
7489 "a published launch is not a running task"
7490 );
7491
7492 runtime.submit(&spawned(
7493 "in-ack-1",
7494 1_700_000_002_000,
7495 &effect_id(started.step_seq),
7496 &["wf-node0"],
7497 ));
7498 assert_eq!(
7499 runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7500 Some(TaskLifecycle::Running),
7501 "only the acknowledgement makes a task Running (§10.4, §15.3)"
7502 );
7503 }
7504
7505 #[test]
7509 fn an_ack_gated_spawn_mints_identity_in_pending_launch_before_the_effect_is_published() {
7510 let mut engine = LoopStateMachine::new(SchedulerBudget::default());
7511 let action = engine.load_workflow(
7512 build_core_spec(&WireSpec {
7513 name: String::new(),
7514 nodes: vec![wire_node("only", "only", &[])],
7515 })
7516 .unwrap(),
7517 );
7518 assert!(matches!(action, LoopAction::SpawnWorkflow { .. }));
7519 assert_eq!(
7520 engine.task_lifecycle("wf-node0"),
7521 Some(TaskLifecycle::PendingLaunch),
7522 "identity is minted, the launch is not published yet"
7523 );
7524
7525 engine.mark_tasks_starting(&["wf-node0".to_string()]);
7526 assert_eq!(
7527 engine.task_lifecycle("wf-node0"),
7528 Some(TaskLifecycle::Starting),
7529 "the launch effect is published; the host has not answered"
7530 );
7531
7532 engine.resolve_workflow_spawn(vec!["wf-node0".to_string()], Vec::new());
7533 assert_eq!(
7534 engine.task_lifecycle("wf-node0"),
7535 Some(TaskLifecycle::Running),
7536 "only the acknowledgement makes it Running"
7537 );
7538 }
7539
7540 #[test]
7541 fn a_failed_launch_ends_the_attempt_and_refuses_any_later_completion_for_it() {
7542 use crate::runtime::kernel::wire::effect::{TaskLaunchFailed, TaskLaunchStatus};
7543
7544 let parallel = WireSpec {
7546 name: "parallel".to_string(),
7547 nodes: vec![
7548 wire_node("left", "left", &[]),
7549 wire_node("right", "right", &[]),
7550 ],
7551 };
7552 let mut runtime = Runtime::new();
7553 runtime.submit(&syscall_config());
7554 let started = runtime.submit(&workflow_start("in-start", 1_700_000_001_000, parallel));
7555
7556 let failure = envelope(
7557 "in-ack-fail",
7558 1_700_000_002_000,
7559 KernelInput::ResolveEffect(ResolveEffect {
7560 effect_id: effect_id(started.step_seq),
7561 outcome: EffectOutcome::Succeeded(EffectSucceeded {
7562 result: EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
7563 attempts: vec![
7564 TaskLaunchOutcome {
7565 task_id: TaskId::new("wf-node0").unwrap(),
7566 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
7567 outcome: TaskLaunchStatus::Failed(TaskLaunchFailed {
7568 failure: super::super::effect::TaskLaunchFailure {
7569 kind:
7570 super::super::effect::HostEffectFailureKind::StorageUnavailable,
7571 message: "no worker".to_string(),
7572 },
7573 }),
7574 },
7575 TaskLaunchOutcome {
7576 task_id: TaskId::new("wf-node1").unwrap(),
7577 attempt_id: WireAttemptId::new("wf-node1:attempt:1").unwrap(),
7578 outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
7579 },
7580 ],
7581 }),
7582 }),
7583 }),
7584 );
7585 runtime.submit(&failure);
7586 assert!(
7587 runtime
7588 .driver
7589 .engine()
7590 .unwrap()
7591 .task_lifecycle("wf-node0")
7592 .is_some_and(|state| state.is_terminal()),
7593 "a failed launch terminates the attempt"
7594 );
7595
7596 let fault = runtime.reject(&child_done_with(
7597 "in-late",
7598 1_700_000_003_000,
7599 "wf-node0",
7600 "wf-node0:attempt:1",
7601 Vec::new(),
7602 ));
7603 assert_eq!(
7604 fault.code,
7605 KernelFaultCode::InvalidAuthority,
7606 "a terminated attempt is a stale causation"
7607 );
7608 }
7609
7610 #[test]
7611 fn a_second_completion_for_a_spent_attempt_carries_no_authority() {
7612 let mut runtime = workflow_root_awaiting_first_child();
7613 runtime.submit(&child_done_with(
7614 "in-done-1",
7615 1_700_000_003_000,
7616 "wf-node0",
7617 "wf-node0:attempt:1",
7618 Vec::new(),
7619 ));
7620 let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7621
7622 let replayed = child_done_with(
7623 "in-done-1-again",
7624 1_700_000_004_000,
7625 "wf-node0",
7626 "wf-node0:attempt:1",
7627 vec![SyscallRequest::AppendWorkflowNodes(
7628 super::super::syscall::AppendWorkflowNodesRequest {
7629 nodes: vec![wire_node("smuggled", "smuggled", &[])],
7630 },
7631 )],
7632 );
7633 let fault = runtime.reject(&replayed);
7634 assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7635 assert_eq!(
7636 runtime.driver.engine().unwrap().workflow_node_count(),
7637 nodes_before,
7638 "a refused completion appends nothing"
7639 );
7640 }
7641
7642 #[test]
7647 fn a_memory_proposal_becomes_a_kernel_authored_write_with_derived_provenance() {
7648 let mut runtime = workflow_root_awaiting_first_child();
7649 let write =
7650 SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7651 proposal: super::super::syscall::MemoryWriteProposal {
7652 name: "source-set".to_string(),
7653 kind: WireMemoryKind::Project,
7654 content: "12 primary sources".to_string(),
7655 description: String::new(),
7656 evidence_refs: Vec::new(),
7657 },
7658 });
7659 let advanced = runtime.submit(&child_done_with(
7660 "in-done-1",
7661 1_700_000_003_000,
7662 "wf-node0",
7663 "wf-node0:attempt:1",
7664 vec![write],
7665 ));
7666
7667 let persisted = advanced
7668 .published_effects()
7669 .iter()
7670 .find(|effect| effect.tag() == EffectKindTag::PersistMemory)
7671 .expect("the proposal published a memory write");
7672 assert_eq!(
7673 persisted.effect_id,
7674 effect_id_at(advanced.step_seq, 0),
7675 "syscall effects mint their own identity from the step they belong to"
7676 );
7677 let EffectKind::PersistMemory(effect) = &persisted.effect else {
7678 panic!("expected a memory write");
7679 };
7680 assert_eq!(effect.binding.binding_id.as_str(), "mem-binding-1");
7681 assert_eq!(
7682 effect.memory.accepted_at_ms,
7683 WireU64::new(1_700_000_003_000),
7684 "provenance time is the envelope's accepted time, never a host clock (DEC-2)"
7685 );
7686 match &effect.memory.causation {
7687 SyscallCausation::ChildAttempt(child) => {
7688 assert_eq!(child.task_id.as_str(), "wf-node0");
7689 assert_eq!(child.attempt_id.as_str(), "wf-node0:attempt:1");
7690 assert_eq!(child.request_seq, 0, "the seq is the list's own order");
7691 }
7692 other => panic!("expected a child-attempt causation, got {other:?}"),
7693 }
7694 let json = serde_json::to_value(&effect.memory).unwrap().to_string();
7696 for forbidden in ["tenant", "author", "trust_level", "record_id", "session"] {
7697 assert!(
7698 !json.contains(forbidden),
7699 "the kernel-authored write leaked {forbidden}"
7700 );
7701 }
7702 }
7703
7704 #[test]
7705 fn a_memory_query_is_clamped_to_the_operations_retrieval_policy() {
7706 let (mut runtime, provider) = agent_awaiting_provider();
7707 let resolved = runtime.submit(&provider_result(
7708 "in-recall",
7709 1_700_000_002_000,
7710 &provider,
7711 vec![tool_call(
7712 "call-1",
7713 crate::context::manager::MEMORY_TOOL_NAME,
7714 json!({"query": "past briefs", "top_k": 999}),
7715 )],
7716 ));
7717 let queried = sole_effect(&resolved);
7718 let EffectKind::QueryMemory(effect) = &queried.effect else {
7719 panic!("expected a memory query, got {:?}", queried.tag());
7720 };
7721 assert_eq!(
7722 effect.requested_k, 4,
7723 "the model cannot widen the operation's retrieval policy by asking for more"
7724 );
7725 assert!(matches!(
7726 effect.query.causation,
7727 SyscallCausation::ProviderTool(_)
7728 ));
7729 let json = serde_json::to_value(&effect.query).unwrap().to_string();
7731 for forbidden in ["session", "tenant", "author", "trust", "agent_id"] {
7732 assert!(
7733 !json.contains(forbidden),
7734 "the kernel-authored query leaked {forbidden}"
7735 );
7736 }
7737 assert_eq!(effect.binding.binding_id.as_str(), "mem-binding-1");
7738 }
7739
7740 fn all_keys(value: &Value, into: &mut BTreeSet<String>) {
7746 match value {
7747 Value::Object(map) => {
7748 for (key, child) in map {
7749 into.insert(key.clone());
7750 all_keys(child, into);
7751 }
7752 }
7753 Value::Array(items) => items.iter().for_each(|item| all_keys(item, into)),
7754 _ => {}
7755 }
7756 }
7757
7758 fn canonical_arc() -> (Vec<Value>, Vec<Value>, Vec<Value>) {
7765 let mut runtime = Runtime::new();
7766 let mut effects = Vec::new();
7767 let mut observations = Vec::new();
7768 let collect = |_runtime: &Runtime,
7769 committed: &CommittedTransition<PlannedStep>,
7770 effects: &mut Vec<Value>,
7771 observations: &mut Vec<Value>| {
7772 for effect in committed.published_effects() {
7773 effects.push(serde_json::to_value(effect).unwrap());
7774 }
7775 for observation in &committed.step.observations {
7776 observations.push(serde_json::to_value(observation).unwrap());
7777 }
7778 };
7779
7780 let configured = runtime.submit(&syscall_config());
7781 collect(&runtime, &configured, &mut effects, &mut observations);
7782
7783 let started = runtime.submit(&workflow_start(
7784 "in-start",
7785 1_700_000_001_000,
7786 two_node_spec(),
7787 ));
7788 collect(&runtime, &started, &mut effects, &mut observations);
7789
7790 let acked = runtime.submit(&spawned(
7791 "in-ack-1",
7792 1_700_000_002_000,
7793 &effect_id(started.step_seq),
7794 &["wf-node0"],
7795 ));
7796 collect(&runtime, &acked, &mut effects, &mut observations);
7797
7798 let write =
7799 SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7800 proposal: super::super::syscall::MemoryWriteProposal {
7801 name: "source-set".to_string(),
7802 kind: WireMemoryKind::Project,
7803 content: "12 primary sources".to_string(),
7804 description: String::new(),
7805 evidence_refs: Vec::new(),
7806 },
7807 });
7808 let advanced = runtime.submit(&child_done_with(
7809 "in-done-1",
7810 1_700_000_003_000,
7811 "wf-node0",
7812 "wf-node0:attempt:1",
7813 vec![write],
7814 ));
7815 collect(&runtime, &advanced, &mut effects, &mut observations);
7816
7817 let records = runtime
7818 .journal
7819 .iter()
7820 .map(|record| {
7821 json!({
7822 "bytes": String::from_utf8_lossy(record.record_bytes().as_slice()).into_owned(),
7823 "digest": record.record_digest().as_str(),
7824 })
7825 })
7826 .collect();
7827 (records, effects, observations)
7828 }
7829
7830 #[test]
7834 fn the_canonical_arc_is_byte_identical_for_any_host() {
7835 let (records_a, effects_a, observations_a) = canonical_arc();
7836 let (records_b, effects_b, observations_b) = canonical_arc();
7837
7838 assert!(!records_a.is_empty() && !effects_a.is_empty() && !observations_a.is_empty());
7839 assert_eq!(records_a, records_b, "the record chain is not reproducible");
7840 assert_eq!(
7841 effects_a, effects_b,
7842 "published effects are not reproducible"
7843 );
7844 assert_eq!(
7845 observations_a, observations_b,
7846 "observations are not reproducible"
7847 );
7848 }
7849
7850 #[test]
7854 fn no_canonical_record_effect_or_observation_names_a_session() {
7855 const BANNED_KEYS: [&str; 5] = [
7856 "session_id",
7857 "parent_session_id",
7858 "session",
7859 "submitter_agent_id",
7860 "actor_id",
7861 ];
7862
7863 let (records, effects, observations) = canonical_arc();
7864 for (surface, values) in [
7865 ("record", &records),
7866 ("effect", &effects),
7867 ("observation", &observations),
7868 ] {
7869 for value in values {
7870 let mut keys = BTreeSet::new();
7871 all_keys(value, &mut keys);
7872 for banned in BANNED_KEYS {
7873 assert!(
7874 !keys.contains(banned),
7875 "canonical {surface} carries the host-owned key {banned:?}: {value}"
7876 );
7877 }
7878 assert!(
7879 !value.to_string().contains("session"),
7880 "canonical {surface} mentions a session: {value}"
7881 );
7882 }
7883 }
7884
7885 let launch = effects
7887 .iter()
7888 .find(|effect| effect["effect"]["kind"] == "spawn_tasks")
7889 .expect("the arc launched a child");
7890 let task = &launch["effect"]["tasks"][0];
7891 assert_eq!(task["task_id"], "wf-node0");
7892 assert_eq!(task["attempt_id"], "wf-node0:attempt:1");
7893 assert!(
7894 task["launch_token"].as_str().is_some_and(|t| !t.is_empty()),
7895 "a child is named by task/attempt/launch token, never by a session"
7896 );
7897 }
7898
7899 #[test]
7903 fn a_spawned_process_is_reported_by_its_logical_parent_task() {
7904 let (_, _, observations) = canonical_arc();
7905 let process = observations
7906 .iter()
7907 .find(|observation| observation["kind"] == "agent_process_changed")
7908 .expect("the arc published a process observation");
7909 assert_eq!(process["agent_id"], "wf-node0");
7910 assert_eq!(process["parent_task_id"], "root");
7911
7912 let spec = agent_run_spec(&LogicalAgentSpec::new("write the brief"));
7913 assert_eq!(spec.identity.session_id.as_str(), NO_HOST_SESSION);
7914 assert!(spec.identity.parent_session_id.is_none());
7915 }
7916
7917 #[test]
7925 fn no_syscall_request_shape_carries_a_self_declared_caller() {
7926 use super::super::syscall::*;
7927
7928 let requests = vec![
7929 SyscallRequest::SubmitWorkflow(SubmitWorkflowRequest {
7930 spec: two_node_spec(),
7931 }),
7932 SyscallRequest::AppendWorkflowNodes(AppendWorkflowNodesRequest {
7933 nodes: vec![wire_node("n", "n", &[])],
7934 }),
7935 SyscallRequest::ActivateSkill(ActivateSkillRequest {
7936 name: "debug".to_string(),
7937 lease_turns: Some(3),
7938 }),
7939 SyscallRequest::UpdateTask(UpdateTaskRequest {
7940 update: WireTaskUpdate::default(),
7941 }),
7942 SyscallRequest::RequestMemoryWrite(RequestMemoryWriteRequest {
7943 proposal: MemoryWriteProposal {
7944 name: "n".to_string(),
7945 kind: WireMemoryKind::Project,
7946 content: "c".to_string(),
7947 description: String::new(),
7948 evidence_refs: Vec::new(),
7949 },
7950 }),
7951 SyscallRequest::RequestMemoryQuery(RequestMemoryQueryRequest {
7952 query: MemoryQueryProposal::default(),
7953 }),
7954 SyscallRequest::PageIn(PageInRequest {
7955 handle_id: super::super::scalar::HandleId::new("h-1").unwrap(),
7956 }),
7957 ];
7958
7959 for request in &requests {
7960 let text = serde_json::to_value(request).unwrap().to_string();
7961 for forbidden in [
7962 "submitter_agent_id",
7963 "actor_id",
7964 "agent_id",
7965 "session_id",
7966 "parent_session_id",
7967 "caller",
7968 "author",
7969 "trust",
7970 ] {
7971 assert!(
7972 !text.contains(forbidden),
7973 "{request:?} still exposes {forbidden}"
7974 );
7975 }
7976 }
7977
7978 let smuggled = r#"{"kind":"append_workflow_nodes","nodes":[],"submitter_agent_id":"root"}"#;
7980 assert!(
7981 serde_json::from_str::<SyscallRequest>(smuggled).is_err(),
7982 "a self-declared submitter must not decode"
7983 );
7984 }
7985
7986 #[test]
7989 fn every_syscall_is_classified_against_the_quarantine_rule() {
7990 use super::super::syscall::*;
7991
7992 let classified = [
7993 (
7994 SyscallRequest::SubmitWorkflow(SubmitWorkflowRequest {
7995 spec: WireSpec::default(),
7996 }),
7997 Some("workflow"),
7998 ),
7999 (
8000 SyscallRequest::AppendWorkflowNodes(AppendWorkflowNodesRequest {
8001 nodes: Vec::new(),
8002 }),
8003 Some("workflow"),
8004 ),
8005 (
8006 SyscallRequest::ActivateSkill(ActivateSkillRequest {
8007 name: String::new(),
8008 lease_turns: None,
8009 }),
8010 Some("capability"),
8011 ),
8012 (
8013 SyscallRequest::RequestMemoryWrite(RequestMemoryWriteRequest {
8014 proposal: MemoryWriteProposal {
8015 name: String::new(),
8016 kind: WireMemoryKind::Project,
8017 content: String::new(),
8018 description: String::new(),
8019 evidence_refs: Vec::new(),
8020 },
8021 }),
8022 Some("memory"),
8023 ),
8024 (
8025 SyscallRequest::RequestMemoryQuery(RequestMemoryQueryRequest {
8026 query: MemoryQueryProposal::default(),
8027 }),
8028 Some("memory"),
8029 ),
8030 (
8031 SyscallRequest::UpdateTask(UpdateTaskRequest {
8032 update: WireTaskUpdate::default(),
8033 }),
8034 None,
8035 ),
8036 (
8037 SyscallRequest::PageIn(PageInRequest {
8038 handle_id: super::super::scalar::HandleId::new("h-1").unwrap(),
8039 }),
8040 None,
8041 ),
8042 ];
8043 for (request, family) in &classified {
8044 assert_eq!(&privileged_family(request), family, "{request:?}");
8045 }
8046 }
8047
8048 #[test]
8049 fn a_page_in_of_a_handle_the_caller_does_not_hold_is_refused() {
8050 let (mut runtime, provider) = agent_awaiting_provider();
8051 runtime.submit(&provider_result(
8052 "in-read",
8053 1_700_000_002_000,
8054 &provider,
8055 vec![tool_call(
8056 "call-1",
8057 crate::context::manager::READ_RESULT_TOOL_NAME,
8058 json!({"call_id": "never-existed"}),
8059 )],
8060 ));
8061 let rejected = rejections(&runtime);
8062 assert_eq!(rejected.len(), 1);
8063 assert_eq!(rejected[0].0, "read_result");
8064 assert!(
8065 rejected[0].2.contains("not reachable"),
8066 "got {:?}",
8067 rejected[0].2
8068 );
8069 assert_eq!(
8070 runtime.pending_effect_kinds(),
8071 vec![EffectKindTag::CallProvider],
8072 "an address the caller does not hold produces no page-in effect; the turn continues"
8073 );
8074 }
8075
8076 const BODY: &str = "the full report body, far larger than this operation keeps resident, \
8083 repeated so it clears the inline threshold by a comfortable margin";
8084
8085 fn body_digest() -> Digest {
8086 super::super::record::canonical_digest(BODY.as_bytes())
8087 }
8088
8089 fn payload_config() -> WireEnvelope {
8092 use crate::runtime::kernel::wire::config::PayloadPolicy;
8093 syscall_config_with(|config| {
8094 config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
8095 config.payload_policy = Some(PayloadPolicy {
8096 inline_threshold_bytes: Some(64),
8097 preview_bytes: Some(32),
8098 });
8099 })
8100 }
8101
8102 fn external_payload(
8103 call_id: &str,
8104 digest: Digest,
8105 original_size: u64,
8106 preview: &str,
8107 ) -> WireToolResultPayload {
8108 external_payload_with(
8109 call_id,
8110 digest,
8111 original_size,
8112 preview,
8113 false,
8114 ToolResultDisposition::Recoverable,
8115 )
8116 }
8117
8118 fn external_payload_with(
8120 call_id: &str,
8121 digest: Digest,
8122 original_size: u64,
8123 preview: &str,
8124 is_error: bool,
8125 disposition: ToolResultDisposition,
8126 ) -> WireToolResultPayload {
8127 WireToolResultPayload::External(super::super::effect::ExternalToolResult {
8128 call_id: CallId::new(call_id).unwrap(),
8129 payload_ref: PayloadRef::new("payload:01J8Y2QK7C4N0V").unwrap(),
8130 digest,
8131 original_size: WireU64::new(original_size),
8132 preview: preview.to_string(),
8133 is_error,
8134 disposition,
8135 })
8136 }
8137
8138 fn payloads_resolved(
8139 id: &str,
8140 at: u64,
8141 effect: &EffectId,
8142 results: Vec<WireToolResultPayload>,
8143 ) -> WireEnvelope {
8144 resolved(
8145 id,
8146 at,
8147 effect,
8148 EffectSuccess::Tools(ToolsSuccess { results }),
8149 )
8150 }
8151
8152 fn agent_awaiting_tool_results() -> (Runtime, EffectId) {
8154 let mut runtime = Runtime::new();
8155 runtime.submit(&payload_config());
8156 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
8157 let acted = runtime.submit(&provider_result(
8158 "in-acted",
8159 1_700_000_002_000,
8160 &effect_id(started.step_seq),
8161 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
8162 ));
8163 let tools = sole_effect(&acted);
8164 assert_eq!(tools.tag(), EffectKindTag::ExecuteTools);
8165 (runtime, tools.effect_id.clone())
8166 }
8167
8168 #[test]
8171 fn an_external_tool_result_lands_as_a_preview_and_an_external_handle() {
8172 let (mut runtime, tools) = agent_awaiting_tool_results();
8173 let committed = runtime.submit(&payloads_resolved(
8174 "in-results",
8175 1_700_000_003_000,
8176 &tools,
8177 vec![external_payload(
8178 "call-1",
8179 body_digest(),
8180 BODY.len() as u64,
8181 "the full report body, far la…",
8182 )],
8183 ));
8184 assert_eq!(
8185 kinds(&committed),
8186 vec![EffectKindTag::CallProvider],
8187 "an external result resumes the turn exactly like an inline one"
8188 );
8189 let EffectKind::CallProvider(provider) = &sole_effect(&committed).effect else {
8190 panic!("external residency must resume through the provider");
8191 };
8192 assert!(
8193 provider
8194 .tools
8195 .iter()
8196 .any(|tool| tool.name.as_str() == READ_RESULT_TOOL_NAME),
8197 "the refreshed provider projection must advertise the newly reachable payload"
8198 );
8199
8200 let engine = runtime.driver.engine().expect("the arc built an engine");
8201 assert_eq!(
8202 engine.ctx.payload_residency("call-1"),
8203 Some(&Residency::External {
8204 payload_ref: "payload:01J8Y2QK7C4N0V".to_string(),
8205 digest: body_digest().as_str().to_string(),
8206 original_size: BODY.len() as u64,
8207 }),
8208 "§7.10 rule 3 · the P3 handle is where the reference lives"
8209 );
8210
8211 let rendered = serde_json::to_string(&engine.ctx.partitions.history.messages).unwrap();
8212 assert!(
8213 rendered.contains("the full report body, far la"),
8214 "the preview is what occupies working context"
8215 );
8216 assert!(
8217 !rendered.contains("clears the inline threshold"),
8218 "the body must not be in context: {rendered}"
8219 );
8220 assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8221 assert_eq!(
8222 runtime
8223 .observations()
8224 .iter()
8225 .filter(|observation| matches!(
8226 observation,
8227 KernelObservation::CheckpointTaken { .. }
8228 ))
8229 .count(),
8230 1,
8231 "re-projecting external residency must not execute the provider-call boundary twice",
8232 );
8233 }
8234
8235 #[test]
8238 fn an_external_tool_result_the_kernel_cannot_verify_is_refused() {
8239 let (mut runtime, tools) = agent_awaiting_tool_results();
8240 let fault = runtime.reject(&payloads_resolved(
8241 "in-results",
8242 1_700_000_003_000,
8243 &tools,
8244 vec![external_payload(
8245 "call-1",
8246 Digest::new("md5:deadbeef").unwrap(),
8247 BODY.len() as u64,
8248 "preview",
8249 )],
8250 ));
8251 assert_eq!(fault.code, KernelFaultCode::MalformedEnvelope);
8252 assert!(
8253 fault.message.contains("sha256:<64 hex>"),
8254 "the refusal names the only digest shape a page-in can be checked against: {}",
8255 fault.message
8256 );
8257 }
8258
8259 #[test]
8264 fn an_external_tool_result_below_the_threshold_is_refused() {
8265 let (mut runtime, tools) = agent_awaiting_tool_results();
8266 let small = "tiny";
8267 let fault = runtime.reject(&payloads_resolved(
8268 "in-results",
8269 1_700_000_003_000,
8270 &tools,
8271 vec![external_payload(
8272 "call-1",
8273 super::super::record::canonical_digest(small.as_bytes()),
8274 small.len() as u64,
8275 small,
8276 )],
8277 ));
8278 assert_eq!(fault.code, KernelFaultCode::MalformedEnvelope);
8279 assert!(
8280 fault.message.contains("inlines below 64"),
8281 "{}",
8282 fault.message
8283 );
8284 }
8285
8286 #[test]
8288 fn an_external_preview_over_the_resident_budget_is_refused() {
8289 let (mut runtime, tools) = agent_awaiting_tool_results();
8290 let fault = runtime.reject(&payloads_resolved(
8291 "in-results",
8292 1_700_000_003_000,
8293 &tools,
8294 vec![external_payload(
8295 "call-1",
8296 body_digest(),
8297 BODY.len() as u64,
8298 BODY,
8299 )],
8300 ));
8301 assert_eq!(fault.code, KernelFaultCode::ResourceLimitExceeded);
8302 assert!(fault.message.contains("preview"), "{}", fault.message);
8303 }
8304
8305 #[test]
8310 fn an_inline_tool_result_over_the_threshold_is_refused() {
8311 let (mut runtime, tools) = agent_awaiting_tool_results();
8312 let fault = runtime.reject(&tools_resolved(
8313 "in-results",
8314 1_700_000_003_000,
8315 &tools,
8316 &[("call-1", BODY, false)],
8317 ));
8318 assert_eq!(fault.code, KernelFaultCode::ResourceLimitExceeded);
8319 assert!(
8320 fault.message.contains("externalises at 64"),
8321 "{}",
8322 fault.message
8323 );
8324 assert_eq!(
8325 runtime.pending_effect_kinds(),
8326 vec![EffectKindTag::ExecuteTools],
8327 "a rejected batch leaves the effect it was answering pending — zero mutation"
8328 );
8329 }
8330
8331 #[test]
8333 fn one_illegal_result_rejects_the_whole_batch() {
8334 let mut runtime = Runtime::new();
8335 runtime.submit(&payload_config());
8336 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
8337 let acted = runtime.submit(&provider_result(
8338 "in-acted",
8339 1_700_000_002_000,
8340 &effect_id(started.step_seq),
8341 vec![
8342 tool_call("call-1", "search", json!({"q": "a"})),
8343 tool_call("call-2", "search", json!({"q": "b"})),
8344 ],
8345 ));
8346 let tools = sole_effect(&acted).effect_id.clone();
8347 runtime.reject(&payloads_resolved(
8348 "in-results",
8349 1_700_000_003_000,
8350 &tools,
8351 vec![
8352 WireToolResultPayload::Inline(InlineToolResult {
8353 call_id: CallId::new("call-1").unwrap(),
8354 result: WireToolResult {
8355 output: "small and legal".to_string(),
8356 is_error: false,
8357 disposition: ToolResultDisposition::Recoverable,
8358 tokens: None,
8359 },
8360 }),
8361 external_payload("call-2", Digest::new("md5:deadbeef").unwrap(), 9_000, "p"),
8362 ],
8363 ));
8364 let engine = runtime.driver.engine().expect("the arc built an engine");
8365 assert!(
8366 engine.ctx.payload_residency("call-1").is_none(),
8367 "the legal half of a refused batch must not have landed either"
8368 );
8369 }
8370
8371 #[test]
8374 fn a_page_in_of_an_external_payload_loads_and_verifies_the_body() {
8375 let (mut runtime, tools) = agent_awaiting_tool_results();
8376 let stored = runtime.submit(&payloads_resolved(
8377 "in-results",
8378 1_700_000_003_000,
8379 &tools,
8380 vec![external_payload(
8381 "call-1",
8382 body_digest(),
8383 BODY.len() as u64,
8384 "the full report body, far la…",
8385 )],
8386 ));
8387
8388 let read = runtime.submit(&provider_result(
8389 "in-read",
8390 1_700_000_004_000,
8391 &effect_id(stored.step_seq),
8392 vec![tool_call(
8393 "call-2",
8394 READ_RESULT_TOOL_NAME,
8395 json!({"call_id": "call-1"}),
8396 )],
8397 ));
8398 let load = sole_effect(&read);
8399 assert_eq!(load.tag(), EffectKindTag::LoadPayload);
8400 let EffectKind::LoadPayload(effect) = &load.effect else {
8401 panic!("expected a payload load");
8402 };
8403 assert_eq!(effect.handle_id.as_str(), "call-1");
8404 assert_eq!(
8405 effect.payload_ref.as_str(),
8406 "payload:01J8Y2QK7C4N0V",
8407 "the effect hands back the host's own opaque locator, unread"
8408 );
8409 let load = load.effect_id.clone();
8410 assert!(
8411 rejections(&runtime).is_empty(),
8412 "a reachable, externally-backed handle is a page-in, not a refusal"
8413 );
8414
8415 let restored = runtime.submit(&resolved(
8416 "in-loaded",
8417 1_700_000_005_000,
8418 &load,
8419 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8420 handle_id: HandleId::new("call-1").unwrap(),
8421 payload: InlinePayload {
8422 content: BODY.to_string(),
8423 digest: body_digest(),
8424 original_size: WireU64::new(BODY.len() as u64),
8425 },
8426 }),
8427 ));
8428 assert_eq!(
8429 kinds(&restored),
8430 vec![EffectKindTag::CallProvider],
8431 "the paged-in body resumes the turn that asked for it"
8432 );
8433
8434 let engine = runtime.driver.engine().expect("the arc built an engine");
8435 assert_eq!(
8436 engine.ctx.payload_residency("call-1"),
8437 Some(&Residency::Resident),
8438 "§25.9 · the handle is the fact, and it says the body came home"
8439 );
8440 let rendered = serde_json::to_string(&engine.ctx.partitions.history.messages).unwrap();
8441 assert!(
8442 rendered.contains("clears the inline threshold"),
8443 "the model reads the body it asked for"
8444 );
8445 assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8446 }
8447
8448 #[test]
8451 fn a_paged_in_body_that_is_not_the_one_that_left_is_refused() {
8452 let (mut runtime, load) = agent_awaiting_payload_load();
8453 let substitute = BODY.replace("margin", "MARGIN");
8456 assert_eq!(substitute.len(), BODY.len());
8457 let fault = runtime.reject(&resolved(
8458 "in-loaded",
8459 1_700_000_005_000,
8460 &load,
8461 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8462 handle_id: HandleId::new("call-1").unwrap(),
8463 payload: InlinePayload {
8464 content: substitute.to_string(),
8465 digest: super::super::record::canonical_digest(substitute.as_bytes()),
8466 original_size: WireU64::new(substitute.len() as u64),
8467 },
8468 }),
8469 ));
8470 assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8471 assert!(fault.message.contains("digests to"), "{}", fault.message);
8472
8473 let engine = runtime.driver.engine().expect("the arc built an engine");
8474 assert!(
8475 matches!(
8476 engine.ctx.payload_residency("call-1"),
8477 Some(Residency::External { .. })
8478 ),
8479 "a refused restore leaves the handle exactly where it was"
8480 );
8481 }
8482
8483 #[test]
8486 fn a_paged_in_body_that_contradicts_its_own_size_is_refused() {
8487 let (mut runtime, load) = agent_awaiting_payload_load();
8488 let fault = runtime.reject(&resolved(
8489 "in-loaded",
8490 1_700_000_005_000,
8491 &load,
8492 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8493 handle_id: HandleId::new("call-1").unwrap(),
8494 payload: InlinePayload {
8495 content: BODY.to_string(),
8496 digest: body_digest(),
8497 original_size: WireU64::new(BODY.len() as u64 + 1),
8498 },
8499 }),
8500 ));
8501 assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8502 assert!(fault.message.contains("and carries"), "{}", fault.message);
8503 }
8504
8505 #[test]
8508 fn a_paged_in_body_for_another_handle_is_refused() {
8509 let (mut runtime, load) = agent_awaiting_payload_load();
8510 let fault = runtime.reject(&resolved(
8511 "in-loaded",
8512 1_700_000_005_000,
8513 &load,
8514 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8515 handle_id: HandleId::new("call-9").unwrap(),
8516 payload: InlinePayload {
8517 content: BODY.to_string(),
8518 digest: body_digest(),
8519 original_size: WireU64::new(BODY.len() as u64),
8520 },
8521 }),
8522 ));
8523 assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8524 assert!(fault.message.contains("names handle"), "{}", fault.message);
8525 }
8526
8527 #[test]
8530 fn a_payload_the_host_cannot_produce_abandons_the_read() {
8531 let (mut runtime, load) = agent_awaiting_payload_load();
8532 let resumed = runtime.submit(&failed(
8533 "in-load-failed",
8534 1_700_000_005_000,
8535 &load,
8536 HostEffectFailureKind::StorageUnavailable,
8537 "the blob store is offline",
8538 ));
8539 assert_eq!(
8540 kinds(&resumed),
8541 vec![EffectKindTag::CallProvider],
8542 "one page-in failure does not kill a live run"
8543 );
8544 assert!(observation_kinds(&runtime).contains(&"payload_load_failed"));
8545 let engine = runtime.driver.engine().expect("the arc built an engine");
8546 assert!(
8547 matches!(
8548 engine.ctx.payload_residency("call-1"),
8549 Some(Residency::External { .. })
8550 ),
8551 "the reference survives the failed read: the body is still out there"
8552 );
8553 }
8554
8555 #[test]
8558 fn a_page_in_of_a_resident_handle_is_refused() {
8559 let (mut runtime, tools) = agent_awaiting_tool_results();
8560 let inlined = runtime.submit(&tools_resolved(
8561 "in-results",
8562 1_700_000_003_000,
8563 &tools,
8564 &[("call-1", "three sources found", false)],
8565 ));
8566 runtime.submit(&provider_result(
8567 "in-read",
8568 1_700_000_004_000,
8569 &effect_id(inlined.step_seq),
8570 vec![tool_call(
8571 "call-2",
8572 READ_RESULT_TOOL_NAME,
8573 json!({"call_id": "call-1"}),
8574 )],
8575 ));
8576 let rejected = rejections(&runtime);
8577 assert_eq!(rejected.len(), 1);
8578 assert_eq!(rejected[0].0, "read_result");
8579 assert!(
8580 rejected[0].2.contains("nothing to page in"),
8581 "got {:?}",
8582 rejected[0].2
8583 );
8584 assert_eq!(
8585 runtime.pending_effect_kinds(),
8586 vec![EffectKindTag::CallProvider],
8587 "a body core already holds publishes no load effect"
8588 );
8589 }
8590
8591 #[test]
8595 fn a_page_out_archive_becomes_a_readable_paged_out_handle() {
8596 let (mut runtime, archive) = agent_awaiting_page_out();
8597 let EffectKind::ArchivePageOut(published) = &runtime
8598 .tx
8599 .pending_effects()
8600 .find(|effect| effect.effect_id == archive)
8601 .expect("the archive is pending")
8602 .effect
8603 else {
8604 panic!("expected a page-out archive");
8605 };
8606 let handle_id = published.handle_id.clone();
8607 let digest = published.payload.digest.clone();
8608 let content = published.payload.content.clone();
8609 let original_size = published.payload.original_size;
8610
8611 let archived = runtime.submit(&resolved(
8612 "in-archived",
8613 1_700_000_003_000,
8614 &archive,
8615 EffectSuccess::PageOutArchived(super::super::effect::PageOutArchivedSuccess {
8616 receipt: ArchiveReceipt {
8617 handle_id: handle_id.clone(),
8618 payload_ref: PayloadRef::new("payload:archive-1").unwrap(),
8619 digest: digest.clone(),
8620 original_size,
8621 },
8622 }),
8623 ));
8624 let engine = runtime.driver.engine().expect("the arc built an engine");
8625 assert_eq!(
8626 engine.ctx.payload_residency(handle_id.as_str()),
8627 Some(&Residency::PagedOut {
8628 payload_ref: "payload:archive-1".to_string(),
8629 digest: digest.as_str().to_string(),
8630 }),
8631 "an archived body is paged out, never external — it *was* resident"
8632 );
8633 assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8634
8635 let read = runtime.submit(&provider_result(
8637 "in-read",
8638 1_700_000_004_000,
8639 &effect_id(archived.step_seq),
8640 vec![tool_call(
8641 "call-7",
8642 READ_RESULT_TOOL_NAME,
8643 json!({ "call_id": handle_id.as_str() }),
8644 )],
8645 ));
8646 let load = sole_effect(&read);
8647 assert_eq!(load.tag(), EffectKindTag::LoadPayload);
8648 let load = load.effect_id.clone();
8649
8650 runtime.submit(&resolved(
8651 "in-loaded",
8652 1_700_000_005_000,
8653 &load,
8654 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8655 handle_id: handle_id.clone(),
8656 payload: InlinePayload {
8657 content: content.clone(),
8658 digest,
8659 original_size,
8660 },
8661 }),
8662 ));
8663 let engine = runtime.driver.engine().expect("the arc built an engine");
8664 assert_eq!(
8665 engine.ctx.payload_residency(handle_id.as_str()),
8666 Some(&Residency::Resident),
8667 "the archived history came home through the same effect the external body uses"
8668 );
8669 }
8670
8671 fn agent_awaiting_payload_load() -> (Runtime, EffectId) {
8673 let (mut runtime, tools) = agent_awaiting_tool_results();
8674 let stored = runtime.submit(&payloads_resolved(
8675 "in-results",
8676 1_700_000_003_000,
8677 &tools,
8678 vec![external_payload(
8679 "call-1",
8680 body_digest(),
8681 BODY.len() as u64,
8682 "the full report body, far la…",
8683 )],
8684 ));
8685 let read = runtime.submit(&provider_result(
8686 "in-read",
8687 1_700_000_004_000,
8688 &effect_id(stored.step_seq),
8689 vec![tool_call(
8690 "call-2",
8691 READ_RESULT_TOOL_NAME,
8692 json!({"call_id": "call-1"}),
8693 )],
8694 ));
8695 (runtime, sole_effect(&read).effect_id.clone())
8696 }
8697
8698 #[test]
8709 fn no_canonical_record_effect_or_observation_carries_an_external_body() {
8710 let (mut runtime, tools) = agent_awaiting_tool_results();
8711 runtime.submit(&payloads_resolved(
8712 "in-results",
8713 1_700_000_003_000,
8714 &tools,
8715 vec![external_payload(
8716 "call-1",
8717 body_digest(),
8718 BODY.len() as u64,
8719 "the full report body, far la…",
8720 )],
8721 ));
8722
8723 let mut surfaces: Vec<(String, String)> = Vec::new();
8726 for record in &runtime.journal {
8727 surfaces.push((
8728 "record".to_string(),
8729 String::from_utf8_lossy(record.record_bytes().as_slice()).into_owned(),
8730 ));
8731 surfaces.push((
8732 "accepted input".to_string(),
8733 serde_json::to_string(&record.normalized_input().expect("the record decodes"))
8734 .unwrap(),
8735 ));
8736 }
8737 for effect in runtime.tx.pending_effects() {
8738 surfaces.push(("effect".to_string(), serde_json::to_string(effect).unwrap()));
8739 }
8740 for observation in runtime.observations() {
8741 surfaces.push((
8742 "observation".to_string(),
8743 serde_json::to_string(observation).unwrap(),
8744 ));
8745 }
8746 assert!(surfaces.len() >= 4, "the arc produced nothing to scan");
8747 for (surface, text) in &surfaces {
8748 assert!(
8749 !text.contains("clears the inline threshold"),
8750 "the canonical {surface} carries the externalised body: {text}"
8751 );
8752 assert!(
8753 !text.contains("spool"),
8754 "the canonical {surface} still speaks of spooling: {text}"
8755 );
8756 }
8757
8758 for tag in EffectKindTag::ALL {
8760 assert!(
8761 !tag.as_str().contains("spool"),
8762 "{} would be the round trip §7.10 deletes",
8763 tag.as_str()
8764 );
8765 }
8766 }
8767
8768 #[test]
8780 fn a_page_in_cannot_address_anything_outside_the_handle_table() {
8781 let (mut runtime, tools) = agent_awaiting_tool_results();
8782 let stored = runtime.submit(&payloads_resolved(
8783 "in-results",
8784 1_700_000_003_000,
8785 &tools,
8786 vec![external_payload(
8787 "call-1",
8788 body_digest(),
8789 BODY.len() as u64,
8790 "the full report body, far la…",
8791 )],
8792 ));
8793 runtime.submit(&provider_result(
8795 "in-read",
8796 1_700_000_004_000,
8797 &effect_id(stored.step_seq),
8798 vec![tool_call(
8799 "call-2",
8800 READ_RESULT_TOOL_NAME,
8801 json!({"call_id": "payload:01J8Y2QK7C4N0V"}),
8802 )],
8803 ));
8804 let rejected = rejections(&runtime);
8805 assert_eq!(rejected.len(), 1);
8806 assert!(
8807 rejected[0].2.contains("not reachable"),
8808 "got {:?}",
8809 rejected[0].2
8810 );
8811 assert_eq!(
8812 runtime.pending_effect_kinds(),
8813 vec![EffectKindTag::CallProvider],
8814 "no effect is published for an address the table does not hold"
8815 );
8816 }
8817
8818 #[test]
8823 fn the_journal_replays_to_byte_identical_records() {
8824 let runtime = drive_workflow_root_to_terminal();
8825 verify_record_chain(&runtime.journal).expect("the chain links up");
8826
8827 let mut replay = CanonicalOperationDriver::new();
8828 let rebuilt: KernelTransaction<PlannedStep, InMemoryRecordIndex> =
8829 KernelTransaction::rebuild_from_records(
8830 &runtime.journal,
8831 ConfigDefaults::default(),
8832 InMemoryRecordIndex::new(),
8833 |context| replay.fold(context),
8834 )
8835 .expect("a deterministic driver rebuilds its own journal");
8836
8837 assert_eq!(rebuilt.lifecycle(), OperationLifecycle::Completed);
8838 assert_eq!(rebuilt.terminal(), runtime.tx.terminal());
8839 assert_eq!(replay.root_kind(), runtime.driver.root_kind());
8840 assert_eq!(replay.focus(), runtime.driver.focus());
8841 }
8842
8843 #[test]
8847 fn a_journal_with_syscall_transitions_replays_to_byte_identical_records() {
8848 let mut runtime = workflow_root_awaiting_first_child();
8849 runtime.submit(&child_done_with(
8850 "in-done-1",
8851 1_700_000_003_000,
8852 "wf-node0",
8853 "wf-node0:attempt:1",
8854 vec![
8855 SyscallRequest::AppendWorkflowNodes(
8856 super::super::syscall::AppendWorkflowNodesRequest {
8857 nodes: vec![wire_node("verify", "verify", &[])],
8858 },
8859 ),
8860 SyscallRequest::AppendWorkflowNodes(
8861 super::super::syscall::AppendWorkflowNodesRequest {
8862 nodes: vec![wire_node("orphan", "orphan", &["nowhere"])],
8863 },
8864 ),
8865 ],
8866 ));
8867 verify_record_chain(&runtime.journal).expect("the chain links up");
8868
8869 let mut replay = CanonicalOperationDriver::new();
8870 let rebuilt: KernelTransaction<PlannedStep, InMemoryRecordIndex> =
8871 KernelTransaction::rebuild_from_records(
8872 &runtime.journal,
8873 ConfigDefaults::default(),
8874 InMemoryRecordIndex::new(),
8875 |context| replay.fold(context),
8876 )
8877 .expect("a deterministic driver rebuilds its own syscall journal");
8878
8879 assert_eq!(rebuilt.lifecycle(), runtime.tx.lifecycle());
8880 assert_eq!(replay.focus(), runtime.driver.focus());
8881 assert_eq!(
8882 replay.engine().unwrap().workflow_node_count(),
8883 runtime.driver.engine().unwrap().workflow_node_count(),
8884 "the appended node is a kernel fact the replay reproduces"
8885 );
8886 assert_eq!(
8887 replay.attempts, runtime.driver.attempts,
8888 "live attempts rebuild identically, so authority after a rebuild is the same"
8889 );
8890 }
8891
8892 #[test]
8893 fn a_plan_that_never_commits_fails_closed_instead_of_drifting() {
8894 let mut runtime = Runtime::new();
8895 runtime.submit(&configure());
8896
8897 let preparation = runtime.prepare(&agent_start("in-start", 1_700_000_001_000));
8899 let token = preparation.token().unwrap().clone();
8900 runtime.tx.abort(&token).expect("abort before the append");
8901
8902 let fault = runtime
8903 .driver
8904 .plan(&PlanContext {
8905 input: &runtime.journal[0].normalized_input().unwrap(),
8906 step_seq: WireU64::new(1),
8907 previous_head: None,
8908 config: runtime.tx.config().unwrap(),
8909 resolving: None,
8910 })
8911 .expect_err("a discarded plan poisons the driver");
8912 assert_eq!(fault.code, KernelFaultCode::TransactionConflict);
8913 assert!(runtime.driver.poison().is_some(), "the driver is poisoned");
8914 }
8915
8916 use crate::runtime::kernel::wire::effect::{
8924 ApprovalSuccess, ArchiveReceipt, EffectFailed, HostEffectFailure, HostEffectFailureKind,
8925 InlinePayload, InlineToolResult, MemoryPersistReceipt, MemoryPersistedSuccess,
8926 MemoryQueriedSuccess, MemoryRecall, MilestoneCheckResult as WireMilestoneResult,
8927 MilestoneEvaluatedSuccess, PageOutArchivedSuccess, PayloadLoadedSuccess,
8928 ProviderContextOverflow, ProviderStopReason, TaskAlreadyFinished, TaskPreemptOutcome,
8929 TaskPreemptStatus, TaskPreempted, TasksPreemptedSuccess, ToolResult as WireToolResult,
8930 ToolsSuccess,
8931 };
8932 use crate::runtime::kernel::wire::scalar::{CallId, HandleId};
8933 use crate::runtime::kernel::wire::syscall::MemoryKind as SyscallMemoryKind;
8934 use crate::runtime::kernel::wire::{Digest, MemoryRecordRef, PayloadRef};
8935
8936 fn support_with(extra: impl IntoIterator<Item = EffectKindTag>) -> HostEffectSupport {
8940 HostEffectSupport::new(
8941 [
8942 EffectKindTag::CallProvider,
8943 EffectKindTag::ExecuteTools,
8944 EffectKindTag::LoadPayload,
8945 EffectKindTag::SpawnTasks,
8946 EffectKindTag::PreemptTasks,
8947 EffectKindTag::PersistMemory,
8948 EffectKindTag::QueryMemory,
8949 ]
8950 .into_iter()
8951 .chain(extra),
8952 )
8953 }
8954
8955 fn resolved(id: &str, at: u64, effect: &EffectId, result: EffectSuccess) -> WireEnvelope {
8956 envelope(
8957 id,
8958 at,
8959 KernelInput::ResolveEffect(ResolveEffect {
8960 effect_id: effect.clone(),
8961 outcome: EffectOutcome::Succeeded(EffectSucceeded { result }),
8962 }),
8963 )
8964 }
8965
8966 fn failed(
8967 id: &str,
8968 at: u64,
8969 effect: &EffectId,
8970 kind: HostEffectFailureKind,
8971 message: &str,
8972 ) -> WireEnvelope {
8973 envelope(
8974 id,
8975 at,
8976 KernelInput::ResolveEffect(ResolveEffect {
8977 effect_id: effect.clone(),
8978 outcome: EffectOutcome::Failed(EffectFailed {
8979 failure: HostEffectFailure {
8980 kind,
8981 message: message.to_string(),
8982 retryable: None,
8983 },
8984 }),
8985 }),
8986 )
8987 }
8988
8989 fn provider_answer(id: &str, at: u64, effect: &EffectId, text: &str) -> WireEnvelope {
8991 resolved(
8992 id,
8993 at,
8994 effect,
8995 EffectSuccess::Provider(super::super::effect::ProviderSuccess {
8996 outcome: ProviderOutcome::Completed(ProviderCompleted {
8997 message: ProviderMessage {
8998 role: MessageRole::Assistant,
8999 content: text.to_string(),
9000 tool_calls: Vec::new(),
9001 tool_call_id: None,
9002 tokens: None,
9003 },
9004 observed_input_tokens: None,
9005 observed_output_tokens: None,
9006 stop_reason: Some(ProviderStopReason::EndTurn),
9007 }),
9008 }),
9009 )
9010 }
9011
9012 fn provider_overflow(id: &str, at: u64, effect: &EffectId) -> WireEnvelope {
9013 resolved(
9014 id,
9015 at,
9016 effect,
9017 EffectSuccess::Provider(super::super::effect::ProviderSuccess {
9018 outcome: ProviderOutcome::ContextOverflow(ProviderContextOverflow {
9019 observed_input_tokens: Some(999_999),
9020 }),
9021 }),
9022 )
9023 }
9024
9025 fn tools_resolved(
9026 id: &str,
9027 at: u64,
9028 effect: &EffectId,
9029 results: &[(&str, &str, bool)],
9030 ) -> WireEnvelope {
9031 resolved(
9032 id,
9033 at,
9034 effect,
9035 EffectSuccess::Tools(ToolsSuccess {
9036 results: results
9037 .iter()
9038 .map(|(call_id, output, is_error)| {
9039 WireToolResultPayload::Inline(InlineToolResult {
9040 call_id: CallId::new(*call_id).unwrap(),
9041 result: WireToolResult {
9042 output: (*output).to_string(),
9043 is_error: *is_error,
9044 disposition: ToolResultDisposition::Recoverable,
9045 tokens: None,
9046 },
9047 })
9048 })
9049 .collect(),
9050 }),
9051 )
9052 }
9053
9054 fn kinds(committed: &CommittedTransition<PlannedStep>) -> Vec<EffectKindTag> {
9055 committed
9056 .published_effects()
9057 .iter()
9058 .map(|effect| effect.tag())
9059 .collect()
9060 }
9061
9062 fn observation_kinds(runtime: &Runtime) -> Vec<&'static str> {
9064 runtime
9065 .observations()
9066 .iter()
9067 .map(observation_label)
9068 .collect()
9069 }
9070
9071 fn observation_label(observation: &KernelObservation) -> &'static str {
9072 match observation {
9073 KernelObservation::MemoryWritten { .. } => "memory_written",
9074 KernelObservation::MemoryWriteFailed { .. } => "memory_write_failed",
9075 KernelObservation::MemoryQueried { .. } => "memory_queried",
9076 KernelObservation::MemoryQueryFailed { .. } => "memory_query_failed",
9077 KernelObservation::PageOutArchived { .. } => "page_out_archived",
9078 KernelObservation::PageOutArchiveFailed { .. } => "page_out_archive_failed",
9079 KernelObservation::ApprovalResolutionFailed { .. } => "approval_resolution_failed",
9080 KernelObservation::AgentPreemptFailed { .. } => "agent_preempt_failed",
9081 KernelObservation::ControlRequestRejected { .. } => "control_request_rejected",
9082 KernelObservation::Compressed { .. } => "compressed",
9083 KernelObservation::WorkflowBatchSpawned { .. } => "workflow_batch_spawned",
9084 KernelObservation::WorkflowCompleted { .. } => "workflow_completed",
9085 KernelObservation::MilestoneAdvanced { .. } => "milestone_advanced",
9086 KernelObservation::MilestoneBlocked { .. } => "milestone_blocked",
9087 KernelObservation::Resumed { .. } => "resumed",
9088 KernelObservation::Suspended { .. } => "suspended",
9089 KernelObservation::SignalDeliveryDisposed { .. } => "signal_delivery_disposed",
9090 KernelObservation::SignalDisplaced { .. } => "signal_displaced",
9091 KernelObservation::SignalExpired { .. } => "signal_expired",
9092 KernelObservation::SignalsPending { .. } => "signals_pending",
9093 KernelObservation::OperationCancelled { .. } => "operation_cancelled",
9094 KernelObservation::LivePolicyChanged { .. } => "live_policy_changed",
9095 KernelObservation::CapabilityChanged { .. } => "capability_changed",
9096 KernelObservation::AgentPreempted { .. } => "agent_preempted",
9097 KernelObservation::PayloadResidencyChanged { .. } => "payload_residency_changed",
9098 KernelObservation::PayloadLoadFailed { .. } => "payload_load_failed",
9099 _ => "other",
9100 }
9101 }
9102
9103 fn dispositions(runtime: &Runtime) -> Vec<(String, String, String, u32)> {
9106 runtime
9107 .observations()
9108 .iter()
9109 .filter_map(|observation| match observation {
9110 KernelObservation::SignalDeliveryDisposed {
9111 disposition,
9112 signal_id,
9113 delivery_id,
9114 attempt,
9115 ..
9116 } => Some((
9117 disposition.clone(),
9118 signal_id.clone(),
9119 delivery_id.clone(),
9120 *attempt,
9121 )),
9122 _ => None,
9123 })
9124 .collect()
9125 }
9126
9127 fn history_text(runtime: &Runtime) -> Vec<String> {
9129 runtime
9130 .driver
9131 .engine()
9132 .map(|engine| {
9133 engine
9134 .ctx
9135 .partitions
9136 .history
9137 .messages
9138 .iter()
9139 .map(|message| format!("{:?}:{}", message.role, message_text(message)))
9140 .collect()
9141 })
9142 .unwrap_or_default()
9143 }
9144
9145 fn message_text(message: &Message) -> String {
9146 match &message.content {
9147 Content::Text(text) => text.clone(),
9148 Content::Parts(parts) => parts
9149 .iter()
9150 .map(|part| match part {
9151 crate::types::message::ContentPart::ToolResult { output, .. } => output.clone(),
9152 other => format!("{other:?}"),
9153 })
9154 .collect::<Vec<_>>()
9155 .join(" "),
9156 }
9157 }
9158
9159 #[test]
9162 fn a_provider_turn_with_host_tool_calls_publishes_execute_tools() {
9163 let (mut runtime, provider) = agent_awaiting_provider();
9164 let dispatched = runtime.submit(&provider_result(
9165 "in-search",
9166 1_700_000_002_000,
9167 &provider,
9168 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
9169 ));
9170 assert_eq!(kinds(&dispatched), vec![EffectKindTag::ExecuteTools]);
9171 let EffectKind::ExecuteTools(execute) = &sole_effect(&dispatched).effect else {
9172 panic!("expected a tool batch");
9173 };
9174 assert_eq!(execute.calls.len(), 1);
9175 assert_eq!(execute.calls[0].name, "search");
9176 assert_eq!(execute.calls[0].call_id.as_str(), "call-1");
9177
9178 let resumed = runtime.submit(&tools_resolved(
9180 "in-results",
9181 1_700_000_003_000,
9182 &effect_id(dispatched.step_seq),
9183 &[("call-1", "three sources found", false)],
9184 ));
9185 assert_eq!(kinds(&resumed), vec![EffectKindTag::CallProvider]);
9186 assert!(
9187 history_text(&runtime)
9188 .iter()
9189 .any(|line| line.contains("three sources found")),
9190 "the tool output is what the next turn reads"
9191 );
9192 }
9193
9194 #[test]
9195 fn a_provider_answer_with_no_tool_calls_commits_the_agent_terminal() {
9196 let (mut runtime, provider) = agent_awaiting_provider();
9197 let finished = runtime.submit(&provider_answer(
9198 "in-final",
9199 1_700_000_002_000,
9200 &provider,
9201 "the brief is written",
9202 ));
9203 let terminal = finished.terminal().expect("a final answer terminates");
9204 let KernelTerminal::Agent(agent) = terminal else {
9205 panic!("expected an agent terminal, got {terminal:?}");
9206 };
9207 assert_eq!(agent.result.termination, WireTermination::Completed);
9208 assert_eq!(
9209 agent.result.final_message.as_ref().unwrap().content,
9210 "the brief is written"
9211 );
9212 assert!(
9213 finished.published_effects().is_empty(),
9214 "§7.12 · a terminal step publishes no effect"
9215 );
9216 }
9217
9218 #[test]
9220 fn the_usage_report_rides_the_terminal_and_only_the_terminal() {
9221 let (mut runtime, provider) = agent_awaiting_provider();
9222 let finished = runtime.submit(&provider_answer(
9223 "in-final",
9224 1_700_000_002_000,
9225 &provider,
9226 "done",
9227 ));
9228 let KernelTerminal::Agent(agent) = finished.terminal().unwrap() else {
9229 panic!("expected an agent terminal");
9230 };
9231 let reported = agent.usage.clone();
9232
9233 let replayed = runtime.prepare(&provider_answer(
9236 "in-final",
9237 1_700_000_002_000,
9238 &provider,
9239 "done",
9240 ));
9241 let RecordPreparation::Replayed(replay) = replayed else {
9242 panic!("an exact replay is a replay, not a new step");
9243 };
9244 let Some(committed_step) = &replay.committed_step else {
9245 panic!("a replay above the checkpoint floor carries its step");
9246 };
9247 let StepDisposition::Terminal(terminal) = &committed_step.disposition else {
9248 panic!("the replayed step is the terminal one");
9249 };
9250 let KernelTerminal::Agent(replayed_agent) = &terminal.terminal else {
9251 panic!("expected an agent terminal");
9252 };
9253 assert_eq!(replayed_agent.usage, reported, "one report, one terminal");
9254 assert_eq!(replay.step_seq, finished.step_seq);
9255 }
9256
9257 #[test]
9259 fn a_syscall_only_turn_continues_with_another_provider_call() {
9260 let (mut runtime, provider) = agent_awaiting_provider();
9261 let continued = runtime.submit(&provider_result(
9262 "in-plan",
9263 1_700_000_002_000,
9264 &provider,
9265 vec![
9266 tool_call("call-1", "skill", json!({"name": "debug"})),
9267 tool_call(
9268 "call-2",
9269 "update_plan",
9270 json!({"progress": "sources listed"}),
9271 ),
9272 ],
9273 ));
9274
9275 assert_eq!(
9276 kinds(&continued),
9277 vec![EffectKindTag::CallProvider],
9278 "a pure control-plane batch publishes no effect of its own, so the kernel continues \
9279 the turn itself rather than leaving the operation with nothing outstanding"
9280 );
9281 let history = history_text(&runtime);
9282 assert!(
9283 history.iter().any(|line| line.contains("skill activated")),
9284 "every syscall the kernel executed is answered so the transcript pairs: {history:?}"
9285 );
9286 assert!(
9287 history.iter().any(|line| line.contains("plan updated")),
9288 "{history:?}"
9289 );
9290 assert!(
9292 runtime
9293 .driver
9294 .engine()
9295 .unwrap()
9296 .ctx
9297 .partitions
9298 .history
9299 .messages
9300 .iter()
9301 .any(|message| message.tool_calls.len() == 2),
9302 "the model reads back the turn it actually emitted"
9303 );
9304 }
9305
9306 #[test]
9307 fn a_syscall_batch_that_published_an_effect_waits_instead_of_re_asking() {
9308 let (mut runtime, provider) = agent_awaiting_provider();
9309 let queried = runtime.submit(&provider_result(
9310 "in-memory",
9311 1_700_000_002_000,
9312 &provider,
9313 vec![tool_call(
9314 "call-1",
9315 crate::context::manager::MEMORY_TOOL_NAME,
9316 json!({"query": "prior briefs"}),
9317 )],
9318 ));
9319 assert_eq!(
9320 kinds(&queried),
9321 vec![EffectKindTag::QueryMemory],
9322 "the turn resumes when the recall it asked for resolves; a provider call now would \
9323 race the very facts it was issued to read"
9324 );
9325 }
9326
9327 #[test]
9328 fn a_mixed_batch_adjudicates_the_syscall_and_dispatches_the_tool() {
9329 let (mut runtime, provider) = agent_awaiting_provider();
9330 let mixed = runtime.submit(&provider_result(
9331 "in-mixed",
9332 1_700_000_002_000,
9333 &provider,
9334 vec![
9335 tool_call("call-1", "skill", json!({"name": "debug"})),
9336 tool_call("call-2", "search", json!({"q": "x"})),
9337 ],
9338 ));
9339 assert_eq!(kinds(&mixed), vec![EffectKindTag::ExecuteTools]);
9340 let EffectKind::ExecuteTools(execute) = &sole_effect(&mixed).effect else {
9341 panic!("expected a tool batch");
9342 };
9343 assert_eq!(
9344 execute
9345 .calls
9346 .iter()
9347 .map(|call| call.name.as_str())
9348 .collect::<Vec<_>>(),
9349 vec!["search"],
9350 "a P1 syscall is never dispatched to a host executor"
9351 );
9352 assert!(
9353 history_text(&runtime)
9354 .iter()
9355 .any(|line| line.contains("skill activated")),
9356 "the syscall half still closes its own transcript pair"
9357 );
9358 }
9359
9360 #[test]
9362 fn a_context_overflow_compacts_and_re_asks_without_reading_vendor_text() {
9363 let mut runtime = Runtime::new();
9364 runtime.submit(&syscall_config());
9365 let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9366 let provider = sole_effect(&started).effect_id.clone();
9367
9368 let recovered = runtime.submit(&provider_overflow(
9369 "in-overflow",
9370 1_700_000_002_000,
9371 &provider,
9372 ));
9373 assert_eq!(
9374 kinds(&recovered),
9375 vec![EffectKindTag::CallProvider],
9376 "an overflow is a semantic outcome the kernel recovers from, not a transport failure"
9377 );
9378 assert!(
9379 observation_kinds(&runtime).contains(&"compressed"),
9380 "the recovery ladder ran: {:?}",
9381 observation_kinds(&runtime)
9382 );
9383 }
9384
9385 #[test]
9386 fn canonical_genesis_installs_the_entropy_watch_on_the_engine() {
9387 let mut runtime = Runtime::new();
9388 runtime.submit(&syscall_config_with(|config| {
9389 config.execution_policy = Some(ExecutionPolicy {
9390 max_turns: Some(12),
9391 entropy_watch: Some(EntropyWatchPolicy {
9392 enabled: Some(true),
9393 threshold_ppm: Some(Ppm::new(100_000).unwrap()),
9394 hysteresis_ppm: Some(Ppm::ZERO),
9395 cooldown_turns: Some(0),
9396 notify_model: Some(true),
9397 }),
9398 ..ExecutionPolicy::default()
9399 });
9400 }));
9401 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
9402 let acted = runtime.submit(&provider_result(
9403 "in-acted",
9404 1_700_000_002_000,
9405 &effect_id(started.step_seq),
9406 vec![tool_call("call-1", "search", json!({ "q": "same" }))],
9407 ));
9408 let resolved = runtime.submit(&tools_resolved(
9409 "in-results",
9410 1_700_000_003_000,
9411 &effect_id(acted.step_seq),
9412 &[("call-1", "failed", true)],
9413 ));
9414
9415 assert!(
9416 resolved
9417 .step
9418 .observations
9419 .iter()
9420 .any(|observation| matches!(observation, KernelObservation::EntropyAlert { .. })),
9421 "the resolved canonical execution policy must arm the semantic engine's entropy watch"
9422 );
9423 }
9424
9425 #[test]
9433 fn vendor_error_prose_is_content_and_never_a_recovery_decision() {
9434 const VENDOR_PROSE: &str = "HTTP 413: prompt is too long — context_length_exceeded, \
9435 maximum context length is 128000 tokens";
9436
9437 let mut runtime = Runtime::new();
9439 runtime.submit(&syscall_config());
9440 let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9441 let answered = runtime.submit(&provider_answer(
9442 "in-prose",
9443 1_700_000_002_000,
9444 &sole_effect(&started).effect_id.clone(),
9445 VENDOR_PROSE,
9446 ));
9447 assert!(
9448 answered.terminal().is_some() || kinds(&answered) == vec![EffectKindTag::CallProvider],
9449 "the words are content, not a classification"
9450 );
9451 assert!(
9452 !observation_kinds(&runtime).contains(&"compressed"),
9453 "no recovery ladder ran: {:?}",
9454 observation_kinds(&runtime)
9455 );
9456
9457 let (mut runtime, provider) = agent_awaiting_provider();
9459 let ended = runtime.submit(&failed(
9460 "in-prose-failure",
9461 1_700_000_002_000,
9462 &provider,
9463 HostEffectFailureKind::TransportExhausted,
9464 VENDOR_PROSE,
9465 ));
9466 let Some(KernelTerminal::Failed(failure)) = ended.terminal() else {
9467 panic!("expected a failed terminal, got {:?}", ended.terminal());
9468 };
9469 assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
9470 assert!(
9471 !observation_kinds(&runtime).contains(&"compressed"),
9472 "a failure message is diagnostics; it never selects the recovery ladder: {:?}",
9473 observation_kinds(&runtime)
9474 );
9475 }
9476
9477 #[test]
9484 fn no_canonical_vocabulary_contains_a_vendor_word() {
9485 const VENDOR_WORDS: [&str; 16] = [
9486 "rate_limit",
9487 "429",
9488 "503",
9489 "413",
9490 "overloaded",
9491 "service_unavailable",
9496 "context_length",
9497 "max_context",
9498 "too_long",
9499 "finish_reason",
9500 "openai",
9501 "anthropic",
9502 "gemini",
9503 "deepseek",
9504 "qwen",
9505 "minimax",
9506 ];
9507
9508 let mut vocabulary: Vec<&'static str> = Vec::new();
9509 vocabulary.extend(EffectKindTag::ALL.iter().map(|tag| tag.as_str()));
9510 vocabulary.extend(
9511 super::super::effect::EffectSuccessTag::ALL
9512 .iter()
9513 .map(|tag| tag.as_str()),
9514 );
9515 vocabulary.extend(HostEffectFailureKind::ALL.iter().map(|kind| kind.as_str()));
9516 vocabulary.extend(ProviderStopReason::ALL.iter().map(|r| r.as_str()));
9517 vocabulary.extend(ToolResultDisposition::ALL.iter().map(|d| d.as_str()));
9518 assert!(vocabulary.len() >= 34, "the scan lost a vocabulary");
9519
9520 for word in vocabulary {
9521 for vendor in VENDOR_WORDS {
9522 assert!(
9523 !word.contains(vendor),
9524 "{word:?} carries the vendor word {vendor:?}; the canonical face is the \
9525 host's mapping *target*, never its passthrough"
9526 );
9527 }
9528 }
9529
9530 assert!(
9534 HostEffectFailureKind::ALL
9535 .iter()
9536 .any(|kind| kind.as_str() == "storage_unavailable")
9537 );
9538 assert!(
9539 !HostEffectFailureKind::ALL
9540 .iter()
9541 .any(|kind| kind.as_str().contains("service"))
9542 );
9543 }
9544
9545 #[test]
9546 fn the_overflow_ladder_is_bounded_and_ends_in_an_honest_terminal() {
9547 let mut runtime = Runtime::new();
9548 runtime.submit(&syscall_config_with(|config| {
9549 config.recovery_policy = Some(crate::runtime::kernel::wire::command::RecoveryPolicy {
9550 provider_recovery_attempts: Some(1),
9551 ..Default::default()
9552 });
9553 }));
9554 let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9555 let mut provider = sole_effect(&started).effect_id.clone();
9556
9557 let first = runtime.submit(&provider_overflow("in-of-1", 1_700_000_002_000, &provider));
9558 assert_eq!(kinds(&first), vec![EffectKindTag::CallProvider]);
9559 provider = sole_effect(&first).effect_id.clone();
9560
9561 let exhausted = runtime.submit(&provider_overflow("in-of-2", 1_700_000_003_000, &provider));
9562 let KernelTerminal::Agent(agent) = exhausted.terminal().expect("the ladder is bounded")
9563 else {
9564 panic!("expected an agent terminal");
9565 };
9566 assert_eq!(agent.result.termination, WireTermination::ContextOverflow);
9567 }
9568
9569 #[test]
9571 fn a_provider_failure_commits_a_terminal_and_never_re_asks() {
9572 let (mut runtime, provider) = agent_awaiting_provider();
9573 let ended = runtime.submit(&failed(
9574 "in-dead",
9575 1_700_000_002_000,
9576 &provider,
9577 HostEffectFailureKind::TransportExhausted,
9578 "the vendor returned 503 five times",
9579 ));
9580 assert!(
9581 ended.published_effects().is_empty(),
9582 "DEC-5 · the kernel makes one policy decision and does not re-emit the same intent"
9583 );
9584 let KernelTerminal::Failed(failure) = ended.terminal().expect("a terminal was committed")
9585 else {
9586 panic!("expected a failed terminal, got {:?}", ended.terminal());
9587 };
9588 assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
9589 assert!(
9590 failure.failure.message.contains("transport_exhausted"),
9591 "the classification decides; the prose is only what an operator reads: {}",
9592 failure.failure.message
9593 );
9594 }
9595
9596 #[test]
9598 fn a_tool_batch_failure_answers_every_dispatched_call_and_never_re_runs_it() {
9599 let (mut runtime, provider) = agent_awaiting_provider();
9600 let dispatched = runtime.submit(&provider_result(
9601 "in-search",
9602 1_700_000_002_000,
9603 &provider,
9604 vec![
9605 tool_call("call-1", "search", json!({"q": "a"})),
9606 tool_call("call-2", "search", json!({"q": "b"})),
9607 ],
9608 ));
9609 let tools = effect_id(dispatched.step_seq);
9610
9611 let answered = runtime.submit(&failed(
9612 "in-exec-failed",
9613 1_700_000_003_000,
9614 &tools,
9615 HostEffectFailureKind::PermissionDenied,
9616 "the executor is not allowed to run tools in this sandbox",
9617 ));
9618 assert_eq!(
9619 kinds(&answered),
9620 vec![EffectKindTag::CallProvider],
9621 "the batch is abandoned and the model is asked again — the kernel never re-dispatches \
9622 the same batch"
9623 );
9624 let history = history_text(&runtime);
9625 let refusals = history
9626 .iter()
9627 .filter(|line| line.contains("permission_denied"))
9628 .count();
9629 assert_eq!(
9630 refusals, 2,
9631 "every dispatched call still gets a result: {history:?}"
9632 );
9633 }
9634
9635 fn agent_dispatching_two_tools() -> (Runtime, EffectId) {
9639 let (mut runtime, provider) = agent_awaiting_provider();
9640 let dispatched = runtime.submit(&provider_result(
9641 "in-search",
9642 1_700_000_002_000,
9643 &provider,
9644 vec![
9645 tool_call("call-1", "search", json!({"q": "a"})),
9646 tool_call("call-2", "search", json!({"q": "b"})),
9647 ],
9648 ));
9649 assert_eq!(kinds(&dispatched), vec![EffectKindTag::ExecuteTools]);
9650 let tools = effect_id(dispatched.step_seq);
9651 (runtime, tools)
9652 }
9653
9654 fn tool_batch(
9655 id: &str,
9656 at: u64,
9657 effect: &EffectId,
9658 results: &[(&str, &str, bool, ToolResultDisposition)],
9659 ) -> WireEnvelope {
9660 resolved(
9661 id,
9662 at,
9663 effect,
9664 EffectSuccess::Tools(ToolsSuccess {
9665 results: results
9666 .iter()
9667 .map(|(call_id, output, is_error, disposition)| {
9668 WireToolResultPayload::Inline(InlineToolResult {
9669 call_id: CallId::new(*call_id).unwrap(),
9670 result: WireToolResult {
9671 output: (*output).to_string(),
9672 is_error: *is_error,
9673 disposition: *disposition,
9674 tokens: None,
9675 },
9676 })
9677 })
9678 .collect(),
9679 }),
9680 )
9681 }
9682
9683 #[test]
9684 fn a_fatal_result_closes_out_the_calls_its_batch_never_answered() {
9685 let (mut runtime, tools) = agent_dispatching_two_tools();
9689 let settled = runtime.submit(&tool_batch(
9690 "in-fatal",
9691 1_700_000_003_000,
9692 &tools,
9693 &[(
9694 "call-1",
9695 "disk corrupt, aborting",
9696 true,
9697 ToolResultDisposition::Fatal,
9698 )],
9699 ));
9700 assert_eq!(
9701 kinds(&settled),
9702 vec![EffectKindTag::CallProvider],
9703 "the turn still completes and the model is asked again — a fatal result is model \
9704 feedback, not a rollback (v0.2.42)"
9705 );
9706
9707 let history = history_text(&runtime);
9708 assert!(
9709 history.iter().any(|line| line.contains("disk corrupt")),
9710 "the failure the host reported stays visible: {history:?}"
9711 );
9712 let answered = tool_result_call_ids(&runtime);
9714 assert_eq!(
9715 answered,
9716 vec!["call-1".to_string(), "call-2".to_string()],
9717 "the call the batch never answered is closed out"
9718 );
9719 assert!(
9720 history
9721 .iter()
9722 .any(|line| line.contains("not executed") && line.contains("fatally")),
9723 "the close-out says why the call did not run: {history:?}"
9724 );
9725 }
9726
9727 fn tool_result_call_ids(runtime: &Runtime) -> Vec<String> {
9729 runtime
9730 .driver
9731 .engine()
9732 .map(|engine| {
9733 engine
9734 .ctx
9735 .partitions
9736 .history
9737 .messages
9738 .iter()
9739 .flat_map(|message| match &message.content {
9740 Content::Parts(parts) => parts
9741 .iter()
9742 .filter_map(|part| match part {
9743 crate::types::message::ContentPart::ToolResult {
9744 call_id, ..
9745 } => Some(call_id.to_string()),
9746 _ => None,
9747 })
9748 .collect::<Vec<_>>(),
9749 Content::Text(_) => Vec::new(),
9750 })
9751 .collect()
9752 })
9753 .unwrap_or_default()
9754 }
9755
9756 #[test]
9757 fn an_ordinary_batch_closes_out_nothing() {
9758 let (mut runtime, tools) = agent_dispatching_two_tools();
9761 runtime.submit(&tool_batch(
9762 "in-partial",
9763 1_700_000_003_000,
9764 &tools,
9765 &[(
9766 "call-1",
9767 "no matches",
9768 false,
9769 ToolResultDisposition::Recoverable,
9770 )],
9771 ));
9772 let history = history_text(&runtime);
9773 assert!(
9774 !history.iter().any(|line| line.contains("not executed")),
9775 "a recoverable batch closes nothing out: {history:?}"
9776 );
9777 }
9778
9779 #[test]
9785 fn an_externalised_fatal_stops_the_batch_exactly_as_an_inline_one_does() {
9786 let (mut runtime, tools) = agent_dispatching_two_tools();
9787 let settled = runtime.submit(&resolved(
9788 "in-fatal-external",
9789 1_700_000_003_000,
9790 &tools,
9791 EffectSuccess::Tools(ToolsSuccess {
9792 results: vec![external_payload_with(
9793 "call-1",
9794 Digest::new(
9795 "sha256:3b1f4a7c9e2d05186a4c7f0b9d3e8c25714f6a0b8c5d2e9f1a3b6c8d0e2f4a61",
9796 )
9797 .unwrap(),
9798 524_288,
9799 "Traceback (most recent call last):",
9800 true,
9801 ToolResultDisposition::Fatal,
9802 )],
9803 }),
9804 ));
9805 assert_eq!(kinds(&settled), vec![EffectKindTag::CallProvider]);
9806 assert_eq!(
9807 tool_result_call_ids(&runtime),
9808 vec!["call-1".to_string(), "call-2".to_string()],
9809 "an externalised fatal closes the batch out just like an inline one"
9810 );
9811 let history = history_text(&runtime);
9812 assert!(
9813 history
9814 .iter()
9815 .any(|line| line.contains("not executed") && line.contains("fatally")),
9816 "{history:?}"
9817 );
9818
9819 assert!(
9822 runtime
9823 .driver
9824 .engine()
9825 .expect("engine")
9826 .ctx
9827 .partitions
9828 .history
9829 .messages
9830 .iter()
9831 .any(
9832 |message| matches!(&message.content, Content::Parts(parts) if parts
9833 .iter()
9834 .any(|part| matches!(
9835 part,
9836 crate::types::message::ContentPart::ToolResult {
9837 call_id,
9838 is_error: true,
9839 ..
9840 } if call_id.as_str() == "call-1"
9841 )))
9842 ),
9843 "the externalised failure is committed as an error result"
9844 );
9845 }
9846
9847 #[test]
9848 fn a_fatal_result_does_not_synthesise_an_answer_the_host_already_gave() {
9849 let (mut runtime, tools) = agent_dispatching_two_tools();
9852 runtime.submit(&tool_batch(
9853 "in-fatal-complete",
9854 1_700_000_003_000,
9855 &tools,
9856 &[
9857 ("call-1", "boom", true, ToolResultDisposition::Fatal),
9858 (
9859 "call-2",
9860 "ok anyway",
9861 false,
9862 ToolResultDisposition::Recoverable,
9863 ),
9864 ],
9865 ));
9866 let history = history_text(&runtime);
9867 assert!(
9868 !history.iter().any(|line| line.contains("not executed")),
9869 "every call was answered by the host: {history:?}"
9870 );
9871 assert_eq!(
9872 history
9873 .iter()
9874 .filter(|line| line.contains("ok anyway"))
9875 .count(),
9876 1,
9877 "no call gets two results: {history:?}"
9878 );
9879 }
9880
9881 #[test]
9891 fn a_host_failure_plans_the_same_step_whatever_the_host_advises() {
9892 for kind in HostEffectFailureKind::ALL {
9893 let mut planned: Option<Value> = None;
9894 for retryable in [None, Some(true), Some(false)] {
9895 let (mut runtime, tools) = agent_dispatching_two_tools();
9896 let settled = runtime.submit(&envelope(
9897 "in-failed",
9898 1_700_000_003_000,
9899 KernelInput::ResolveEffect(ResolveEffect {
9900 effect_id: tools.clone(),
9901 outcome: EffectOutcome::Failed(EffectFailed {
9902 failure: HostEffectFailure {
9903 kind,
9904 message: "the executor could not run".to_string(),
9906 retryable,
9907 },
9908 }),
9909 }),
9910 ));
9911 let step = serde_json::to_value(&settled.step).unwrap();
9912 match &planned {
9913 None => planned = Some(step),
9914 Some(first) => assert_eq!(
9915 first, &step,
9916 "{kind:?} with retryable={retryable:?} planned a different step; \
9917 `retryable` is advice and DEC-5 leaves it no branch to select"
9918 ),
9919 }
9920 }
9921 }
9922 }
9923
9924 #[test]
9925 fn the_recovery_decision_reads_the_effect_kind_the_kernel_published() {
9926 for kind in HostEffectFailureKind::ALL {
9930 let (mut runtime, tools) = agent_dispatching_two_tools();
9931 let settled = runtime.submit(&failed(
9932 "in-failed",
9933 1_700_000_003_000,
9934 &tools,
9935 kind,
9936 "the executor could not run",
9937 ));
9938 assert_eq!(
9939 kinds(&settled),
9940 vec![EffectKindTag::CallProvider],
9941 "{kind:?} must take the same decision as every other failure class"
9942 );
9943 assert!(
9944 runtime.pending_effect_kinds() == vec![EffectKindTag::CallProvider],
9945 "{kind:?}: the failed batch is never re-issued (DEC-5)"
9946 );
9947 }
9948 }
9949
9950 #[test]
9952 fn duplicate_and_conflicting_resolutions_are_settled_before_the_driver_sees_them() {
9953 let (mut runtime, provider) = agent_awaiting_provider();
9954 let first = runtime.submit(&provider_result(
9955 "in-skill",
9956 1_700_000_002_000,
9957 &provider,
9958 vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
9959 ));
9960
9961 let replay = runtime.prepare(&provider_result(
9963 "in-skill-again",
9964 1_700_000_002_500,
9965 &provider,
9966 vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
9967 ));
9968 let RecordPreparation::Replayed(replayed) = replay else {
9969 panic!("a semantically identical redelivery is a replay");
9970 };
9971 assert_eq!(replayed.step_seq, first.step_seq);
9972
9973 let conflict = runtime.reject(&provider_result(
9975 "in-skill-conflict",
9976 1_700_000_002_600,
9977 &provider,
9978 vec![tool_call("call-9", "skill", json!({"name": "debug"}))],
9979 ));
9980 assert_eq!(conflict.code, KernelFaultCode::UnexpectedEffectOutcome);
9981
9982 let unknown = EffectId::new("op-driver-1:step:77:effect:0").unwrap();
9984 let stray = runtime.reject(&provider_answer(
9985 "in-stray",
9986 1_700_000_002_700,
9987 &unknown,
9988 "hello",
9989 ));
9990 assert_eq!(stray.code, KernelFaultCode::UnexpectedEffectOutcome);
9991
9992 let pending = effect_id(first.step_seq);
9994 let mismatched = runtime.reject(&tools_resolved(
9995 "in-mismatch",
9996 1_700_000_002_800,
9997 &pending,
9998 &[("call-1", "x", false)],
9999 ));
10000 assert_eq!(mismatched.code, KernelFaultCode::UnexpectedEffectOutcome);
10001 }
10002
10003 #[test]
10007 fn a_spawn_failure_fails_the_whole_batch_and_never_re_launches_it() {
10008 let mut runtime = Runtime::new();
10009 runtime.submit(&configure());
10010 let started = runtime.submit(&workflow_start(
10011 "in-start",
10012 1_700_000_001_000,
10013 two_node_spec(),
10014 ));
10015 let spawn = effect_id(started.step_seq);
10016
10017 let after = runtime.submit(&failed(
10018 "in-launch-failed",
10019 1_700_000_002_000,
10020 &spawn,
10021 HostEffectFailureKind::ResourceExhausted,
10022 "no worker slots",
10023 ));
10024 assert!(
10025 !after
10026 .published_effects()
10027 .iter()
10028 .any(|effect| effect.tag() == EffectKindTag::SpawnTasks),
10029 "DEC-5 · the same launch intent is never re-emitted"
10030 );
10031 let KernelTerminal::Workflow(workflow) = after
10032 .terminal()
10033 .expect("a DAG whose only ready node could not start drains")
10034 else {
10035 panic!("expected a workflow terminal, got {:?}", after.terminal());
10036 };
10037 assert_eq!(workflow.outcome.status, WorkflowStatus::Failed);
10038 assert!(
10039 runtime.driver.attempts.is_empty(),
10040 "a launch that never happened leaves no live attempt to complete later"
10041 );
10042 }
10043
10044 #[test]
10045 fn an_approval_resolution_dispatches_exactly_what_was_approved() {
10046 let (mut runtime, provider) = agent_awaiting_approval();
10047 let requested = runtime.submit(&provider_result(
10048 "in-gated",
10049 1_700_000_002_000,
10050 &provider,
10051 vec![
10052 tool_call("call-1", "search", json!({"q": "a"})),
10053 tool_call("call-2", "search", json!({"q": "b"})),
10054 ],
10055 ));
10056 assert_eq!(kinds(&requested), vec![EffectKindTag::RequestApproval]);
10057 let approval = effect_id(requested.step_seq);
10058
10059 let resumed = runtime.submit(&resolved(
10060 "in-approved",
10061 1_700_000_003_000,
10062 &approval,
10063 EffectSuccess::Approval(ApprovalSuccess {
10064 approved_call_ids: vec![CallId::new("call-1").unwrap()],
10065 denied_call_ids: vec![CallId::new("call-2").unwrap()],
10066 }),
10067 ));
10068 assert_eq!(kinds(&resumed), vec![EffectKindTag::ExecuteTools]);
10069 let EffectKind::ExecuteTools(execute) = &sole_effect(&resumed).effect else {
10070 panic!("expected a tool batch");
10071 };
10072 assert_eq!(
10073 execute
10074 .calls
10075 .iter()
10076 .map(|call| call.call_id.as_str())
10077 .collect::<Vec<_>>(),
10078 vec!["call-1"],
10079 "only the approved call reaches a host"
10080 );
10081 }
10082
10083 #[test]
10085 fn an_approval_failure_denies_every_gated_call_and_never_re_asks() {
10086 let (mut runtime, provider) = agent_awaiting_approval();
10087 let requested = runtime.submit(&provider_result(
10088 "in-gated",
10089 1_700_000_002_000,
10090 &provider,
10091 vec![tool_call("call-1", "search", json!({"q": "a"}))],
10092 ));
10093 let approval = effect_id(requested.step_seq);
10094
10095 let after = runtime.submit(&failed(
10096 "in-approval-failed",
10097 1_700_000_003_000,
10098 &approval,
10099 HostEffectFailureKind::StorageUnavailable,
10100 "the approval queue is down",
10101 ));
10102 assert!(
10103 !after
10104 .published_effects()
10105 .iter()
10106 .any(|effect| effect.tag() == EffectKindTag::RequestApproval),
10107 "DEC-5 · `retry_approval` is deleted on this wire"
10108 );
10109 assert_eq!(kinds(&after), vec![EffectKindTag::CallProvider]);
10110 assert!(
10111 observation_kinds(&runtime).contains(&"approval_resolution_failed"),
10112 "the failure is a typed audit fact: {:?}",
10113 observation_kinds(&runtime)
10114 );
10115 assert!(
10116 history_text(&runtime)
10117 .iter()
10118 .any(|line| line.contains("permission denied")),
10119 "fail closed: an approval that never arrived approves nothing"
10120 );
10121 }
10122
10123 #[test]
10124 fn a_preempt_resolution_settles_every_attempt_it_names() {
10125 let (mut runtime, spawn_effect) = workflow_with_live_child();
10126 let attempts = vec![
10127 TaskPreemptOutcome {
10128 task_id: TaskId::new("wf-node0").unwrap(),
10129 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10130 outcome: TaskPreemptStatus::Preempted(TaskPreempted {}),
10131 },
10132 TaskPreemptOutcome {
10133 task_id: TaskId::new("wf-node1").unwrap(),
10134 attempt_id: WireAttemptId::new("wf-node1:attempt:1").unwrap(),
10135 outcome: TaskPreemptStatus::AlreadyFinished(TaskAlreadyFinished {}),
10136 },
10137 ];
10138 let carrier = syscall_carrier("in-preempt-effect", 1_700_000_004_000);
10142 let published = runtime.submit_planned(&carrier, |driver, context| {
10143 let mut index = 0;
10144 let effect = driver.mint_effect(
10145 context,
10146 EffectKind::PreemptTasks(PreemptTasksEffect {
10147 attempts: vec![TaskAttemptRef {
10148 task_id: TaskId::new("wf-node0").unwrap(),
10149 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10150 }],
10151 reason: "cancelled".to_string(),
10152 }),
10153 &mut index,
10154 );
10155 Ok(PlannedStep {
10156 root_kind: Some(RootKind::Workflow),
10157 focus: driver.focus().cloned(),
10158 observations: Vec::new(),
10159 disposition: StepDisposition::Effects(EffectsDisposition {
10160 effects: vec![effect],
10161 }),
10162 })
10163 });
10164 let preempt = effect_id(published.step_seq);
10165 assert!(runtime.driver.attempts.contains_key("wf-node0"));
10166
10167 runtime.submit(&resolved(
10168 "in-preempted",
10169 1_700_000_005_000,
10170 &preempt,
10171 EffectSuccess::TasksPreempted(TasksPreemptedSuccess { attempts }),
10172 ));
10173 assert!(
10174 !runtime.driver.attempts.contains_key("wf-node0"),
10175 "a preempted attempt is spent, so a later completion naming it is a stale causation"
10176 );
10177 let _ = spawn_effect;
10178 }
10179
10180 #[test]
10182 fn a_preempt_failure_is_an_audit_fact_and_not_a_second_preemption() {
10183 let (mut runtime, _) = workflow_with_live_child();
10184 let carrier = syscall_carrier("in-preempt-effect", 1_700_000_004_000);
10185 let published = runtime.submit_planned(&carrier, |driver, context| {
10186 let mut index = 0;
10187 let effect = driver.mint_effect(
10188 context,
10189 EffectKind::PreemptTasks(PreemptTasksEffect {
10190 attempts: vec![TaskAttemptRef {
10191 task_id: TaskId::new("wf-node0").unwrap(),
10192 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10193 }],
10194 reason: "cancelled".to_string(),
10195 }),
10196 &mut index,
10197 );
10198 Ok(PlannedStep {
10199 root_kind: Some(RootKind::Workflow),
10200 focus: driver.focus().cloned(),
10201 observations: Vec::new(),
10202 disposition: StepDisposition::Effects(EffectsDisposition {
10203 effects: vec![effect],
10204 }),
10205 })
10206 });
10207 let preempt = effect_id(published.step_seq);
10208
10209 let after = runtime.submit(&failed(
10210 "in-preempt-failed",
10211 1_700_000_005_000,
10212 &preempt,
10213 HostEffectFailureKind::Unknown,
10214 "the supervisor did not answer",
10215 ));
10216 assert!(
10217 after.published_effects().is_empty(),
10218 "DEC-5 · `retry_preempt` is deleted on this wire"
10219 );
10220 assert!(
10221 observation_kinds(&runtime).contains(&"agent_preempt_failed"),
10222 "{:?}",
10223 observation_kinds(&runtime)
10224 );
10225 }
10226
10227 #[test]
10228 fn a_milestone_verdict_advances_the_contract_it_belongs_to() {
10229 let (mut runtime, provider) = agent_awaiting_milestone_check();
10230 let requested = runtime.submit(&provider_answer(
10231 "in-claim",
10232 1_700_000_002_000,
10233 &provider,
10234 "phase one is done",
10235 ));
10236 assert_eq!(kinds(&requested), vec![EffectKindTag::EvaluateMilestone]);
10237 let milestone = effect_id(requested.step_seq);
10238
10239 let blocked = runtime.submit(&resolved(
10240 "in-verdict",
10241 1_700_000_003_000,
10242 &milestone,
10243 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10244 result: WireMilestoneResult {
10245 phase_id: "collect".to_string(),
10246 passed: false,
10247 failed_criteria: vec!["no sources cited".to_string()],
10248 score: None,
10249 notes: String::new(),
10250 },
10251 }),
10252 ));
10253 assert_eq!(kinds(&blocked), vec![EffectKindTag::CallProvider]);
10254 assert!(
10255 observation_kinds(&runtime).contains(&"milestone_blocked"),
10256 "{:?}",
10257 observation_kinds(&runtime)
10258 );
10259 }
10260
10261 #[test]
10269 fn a_declared_contract_drives_the_whole_milestone_cascade() {
10270 let (mut runtime, provider) = agent_awaiting_milestone_check();
10271
10272 let requested = runtime.submit(&provider_answer(
10274 "in-claim",
10275 1_700_000_002_000,
10276 &provider,
10277 "sources collected",
10278 ));
10279 assert_eq!(kinds(&requested), vec![EffectKindTag::EvaluateMilestone]);
10280 let EffectKind::EvaluateMilestone(evaluate) = &sole_effect(&requested).effect else {
10281 panic!("expected a milestone request");
10282 };
10283 assert_eq!(
10284 evaluate.request.phase_id, "collect",
10285 "the request names the phase the declared cascade is on"
10286 );
10287 assert_eq!(
10288 evaluate.request.contract_id, "brief-quality-v1",
10289 "a phase id is unique only inside its contract, so the request carries the pair the \
10290 host looks its verifier up by"
10291 );
10292 let request = serde_json::to_value(&evaluate.request).unwrap();
10294 assert_eq!(
10295 request.as_object().unwrap().keys().collect::<Vec<_>>(),
10296 vec!["contract_id", "phase_id"],
10297 "{request}"
10298 );
10299
10300 let advanced = runtime.submit(&resolved(
10302 "in-verdict",
10303 1_700_000_003_000,
10304 &effect_id(requested.step_seq),
10305 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10306 result: WireMilestoneResult {
10307 phase_id: "collect".to_string(),
10308 passed: true,
10309 failed_criteria: Vec::new(),
10310 score: None,
10311 notes: String::new(),
10312 },
10313 }),
10314 ));
10315 assert_eq!(kinds(&advanced), vec![EffectKindTag::CallProvider]);
10316 let advance = runtime
10317 .observations()
10318 .iter()
10319 .find_map(|observation| match observation {
10320 KernelObservation::MilestoneAdvanced {
10321 phase_id,
10322 capabilities_unlocked,
10323 ..
10324 } => Some((phase_id.clone(), capabilities_unlocked.clone())),
10325 _ => None,
10326 })
10327 .unwrap_or_else(|| panic!("{:?}", observation_kinds(&runtime)));
10328 assert_eq!(advance.0, "collect");
10329 assert_eq!(
10330 advance.1,
10331 vec!["Tool:search".to_string()],
10332 "the phase's declared unlocks are the ones mounted"
10333 );
10334 assert_eq!(
10335 runtime
10336 .driver
10337 .engine()
10338 .expect("engine")
10339 .current_milestone_phase_id(),
10340 Some("write"),
10341 "the cascade advanced to the next declared phase"
10342 );
10343
10344 let requested_2 = runtime.submit(&provider_answer(
10346 "in-claim-2",
10347 1_700_000_004_000,
10348 &effect_id(advanced.step_seq),
10349 "brief written",
10350 ));
10351 assert_eq!(kinds(&requested_2), vec![EffectKindTag::EvaluateMilestone]);
10352 runtime.submit(&resolved(
10353 "in-verdict-2",
10354 1_700_000_005_000,
10355 &effect_id(requested_2.step_seq),
10356 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10357 result: WireMilestoneResult {
10358 phase_id: "write".to_string(),
10359 passed: true,
10360 failed_criteria: Vec::new(),
10361 score: None,
10362 notes: String::new(),
10363 },
10364 }),
10365 ));
10366 let unlocked_by_phase_two =
10367 runtime
10368 .observations()
10369 .iter()
10370 .find_map(|observation| match observation {
10371 KernelObservation::MilestoneAdvanced {
10372 capabilities_unlocked,
10373 ..
10374 } => Some(capabilities_unlocked.clone()),
10375 _ => None,
10376 });
10377 assert_eq!(unlocked_by_phase_two, Some(vec!["Skill:debug".to_string()]));
10378 }
10379
10380 #[test]
10381 fn a_run_spec_naming_an_undeclared_contract_is_refused_before_anything_moves() {
10382 let mut runtime = Runtime::new();
10386 runtime.submit(&syscall_config_with(|config| {
10387 config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10388 config.verification_contracts = vec![brief_contract()];
10389 }));
10390 let head_before = runtime.tx.head();
10391
10392 let fault = runtime.reject(&agent_start_under_contract(
10393 "in-start",
10394 1_700_000_001_000,
10395 "brief-quality-v2",
10396 ));
10397 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
10398 assert!(
10399 fault.message.contains("brief-quality-v2"),
10400 "{}",
10401 fault.message
10402 );
10403 assert_eq!(runtime.tx.head(), head_before, "nothing moved");
10404 assert!(
10405 runtime.driver.root_kind().is_none(),
10406 "the operation is still free to start with a spec that resolves"
10407 );
10408
10409 runtime.submit(&agent_start_under_contract(
10411 "in-start-ok",
10412 1_700_000_001_500,
10413 "brief-quality-v1",
10414 ));
10415 assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
10416 }
10417
10418 #[test]
10419 fn a_workflow_node_may_not_name_an_undeclared_contract_either() {
10420 let mut runtime = Runtime::new();
10422 runtime.submit(&syscall_config_with(|config| {
10423 config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10424 config.verification_contracts = vec![brief_contract()];
10425 }));
10426 let mut spec = two_node_spec();
10427 spec.nodes[1].run_spec = Some(LogicalAgentSpec {
10428 verification_contract_id: Some("no-such-contract".to_string()),
10429 ..LogicalAgentSpec::new("write the brief")
10430 });
10431 let fault = runtime.reject(&workflow_start("in-start", 1_700_000_001_000, spec));
10432 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
10433 assert!(
10434 fault.message.contains("no-such-contract"),
10435 "{}",
10436 fault.message
10437 );
10438 }
10439
10440 #[test]
10442 fn a_milestone_that_could_not_be_evaluated_terminates_instead_of_advancing() {
10443 let (mut runtime, provider) = agent_awaiting_milestone_check();
10444 let requested = runtime.submit(&provider_answer(
10445 "in-claim",
10446 1_700_000_002_000,
10447 &provider,
10448 "phase one is done",
10449 ));
10450 let milestone = effect_id(requested.step_seq);
10451
10452 let ended = runtime.submit(&failed(
10453 "in-verifier-down",
10454 1_700_000_003_000,
10455 &milestone,
10456 HostEffectFailureKind::StorageUnavailable,
10457 "the verifier could not be reached",
10458 ));
10459 assert!(ended.published_effects().is_empty());
10460 let KernelTerminal::Failed(failure) = ended.terminal().expect("a terminal was committed")
10461 else {
10462 panic!("expected a failed terminal, got {:?}", ended.terminal());
10463 };
10464 assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
10465 assert!(
10466 failure.failure.message.contains("evaluate_milestone"),
10467 "{}",
10468 failure.failure.message
10469 );
10470 }
10471
10472 #[test]
10476 fn a_memory_receipt_cannot_restate_what_the_kernel_authored() {
10477 let (mut runtime, effect) = agent_awaiting_memory_write();
10478 let settled = runtime.submit(&resolved(
10479 "in-persisted",
10480 1_700_000_003_000,
10481 &effect,
10482 EffectSuccess::MemoryPersisted(MemoryPersistedSuccess {
10483 receipt: MemoryPersistReceipt {
10484 binding_id: MemoryBindingId::new("some-other-binding").unwrap(),
10485 record_ref: MemoryRecordRef::new("rec-7").unwrap(),
10486 digest: Digest::new("sha256:".to_string() + &"0".repeat(64)).unwrap(),
10487 },
10488 }),
10489 ));
10490 assert!(
10491 settled.published_effects().is_empty(),
10492 "a persisted record is a fact, not a new obligation"
10493 );
10494 let written = runtime
10495 .observations()
10496 .iter()
10497 .find_map(|observation| match observation {
10498 KernelObservation::MemoryWritten {
10499 record_id,
10500 scope,
10501 name,
10502 memory_kind,
10503 size_bytes,
10504 ..
10505 } => Some((
10506 record_id.clone(),
10507 scope.clone(),
10508 name.clone(),
10509 *memory_kind,
10510 *size_bytes,
10511 )),
10512 _ => None,
10513 })
10514 .expect("the resolution records the write");
10515 assert_eq!(written.0, "rec-7", "the host contributes its own locator");
10516 assert_eq!(
10517 written.1.namespace, "mem-binding-1",
10518 "and nothing else: the binding is the one the operation holds, not the one echoed back"
10519 );
10520 assert_eq!(written.2, "brief-style");
10521 assert_eq!(written.3, crate::mm::memory::MemoryKind::Project);
10522 assert_eq!(written.4, "prefers numbered sections".len() as u32);
10523 }
10524
10525 #[test]
10526 fn a_memory_write_failure_names_the_intent_the_kernel_authored() {
10527 let (mut runtime, effect) = agent_awaiting_memory_write();
10528 let settled = runtime.submit(&failed(
10529 "in-persist-failed",
10530 1_700_000_003_000,
10531 &effect,
10532 HostEffectFailureKind::StorageUnavailable,
10533 "the memory store is offline",
10534 ));
10535 assert!(settled.published_effects().is_empty());
10536 assert!(
10537 observation_kinds(&runtime).contains(&"memory_write_failed"),
10538 "{:?}",
10539 observation_kinds(&runtime)
10540 );
10541 }
10542
10543 #[test]
10544 fn a_memory_recall_enters_context_before_the_turn_resumes() {
10545 let (mut runtime, provider) = agent_awaiting_provider();
10546 let queried = runtime.submit(&provider_result(
10547 "in-memory",
10548 1_700_000_002_000,
10549 &provider,
10550 vec![tool_call(
10551 "call-1",
10552 crate::context::manager::MEMORY_TOOL_NAME,
10553 json!({"query": "prior briefs", "top_k": 9}),
10554 )],
10555 ));
10556 let query_effect = sole_effect(&queried);
10557 let EffectKind::QueryMemory(query) = &query_effect.effect else {
10558 panic!("expected a memory query");
10559 };
10560 assert_eq!(
10561 query.requested_k, 4,
10562 "retrieval width is the operation's policy, clamped — a model cannot widen it"
10563 );
10564 let effect = query_effect.effect_id.clone();
10565
10566 let resumed = runtime.submit(&resolved(
10567 "in-recalls",
10568 1_700_000_003_000,
10569 &effect,
10570 EffectSuccess::MemoryQueried(MemoryQueriedSuccess {
10571 recalls: vec![MemoryRecall {
10572 record_ref: MemoryRecordRef::new("rec-1").unwrap(),
10573 name: "brief-style".to_string(),
10574 kind: SyscallMemoryKind::Project,
10575 content: "prefers numbered sections".to_string(),
10576 score: None,
10577 }],
10578 }),
10579 ));
10580 assert_eq!(kinds(&resumed), vec![EffectKindTag::CallProvider]);
10581 assert!(
10582 history_text(&runtime)
10583 .iter()
10584 .any(|line| line.contains("prefers numbered sections")),
10585 "the recall is in the context the resumed turn renders"
10586 );
10587 assert!(
10588 observation_kinds(&runtime).contains(&"memory_queried"),
10589 "{:?}",
10590 observation_kinds(&runtime)
10591 );
10592 }
10593
10594 #[test]
10595 fn a_memory_query_failure_resumes_the_turn_without_recalls() {
10596 let (mut runtime, provider) = agent_awaiting_provider();
10597 let queried = runtime.submit(&provider_result(
10598 "in-memory",
10599 1_700_000_002_000,
10600 &provider,
10601 vec![tool_call(
10602 "call-1",
10603 crate::context::manager::MEMORY_TOOL_NAME,
10604 json!({"query": "prior briefs"}),
10605 )],
10606 ));
10607 let effect = effect_id(queried.step_seq);
10608
10609 let resumed = runtime.submit(&failed(
10610 "in-query-failed",
10611 1_700_000_003_000,
10612 &effect,
10613 HostEffectFailureKind::StorageUnavailable,
10614 "the memory store is offline",
10615 ));
10616 assert_eq!(
10617 kinds(&resumed),
10618 vec![EffectKindTag::CallProvider],
10619 "a store that could not answer is not a reason to stall the run"
10620 );
10621 assert!(
10622 observation_kinds(&runtime).contains(&"memory_query_failed"),
10623 "{:?}",
10624 observation_kinds(&runtime)
10625 );
10626 }
10627
10628 #[test]
10629 fn a_page_out_archive_holds_the_continuation_until_the_host_commits_it() {
10630 let (mut runtime, archive) = agent_awaiting_page_out();
10631 let EffectKind::ArchivePageOut(published) = &runtime
10632 .tx
10633 .pending_effects()
10634 .find(|effect| effect.effect_id == archive)
10635 .expect("the archive is pending")
10636 .effect
10637 .clone()
10638 else {
10639 panic!("expected a page-out effect");
10640 };
10641
10642 let resumed = runtime.submit(&resolved(
10643 "in-archived",
10644 1_700_000_004_000,
10645 &archive,
10646 EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
10647 receipt: ArchiveReceipt {
10648 handle_id: published.handle_id.clone(),
10649 payload_ref: PayloadRef::new("blob-1").unwrap(),
10650 digest: published.payload.digest.clone(),
10651 original_size: published.payload.original_size,
10652 },
10653 }),
10654 ));
10655 assert_eq!(
10656 kinds(&resumed),
10657 vec![EffectKindTag::CallProvider],
10658 "the provider retry the compaction deferred is released by the archive's commit"
10659 );
10660 assert!(
10661 observation_kinds(&runtime).contains(&"page_out_archived"),
10662 "{:?}",
10663 observation_kinds(&runtime)
10664 );
10665 }
10666
10667 #[test]
10668 fn an_archive_receipt_for_another_body_is_refused() {
10669 let (mut runtime, archive) = agent_awaiting_page_out();
10670 let fault = runtime.reject(&resolved(
10671 "in-wrong-archive",
10672 1_700_000_004_000,
10673 &archive,
10674 EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
10675 receipt: ArchiveReceipt {
10676 handle_id: HandleId::new("some-other-handle").unwrap(),
10677 payload_ref: PayloadRef::new("blob-1").unwrap(),
10678 digest: Digest::new("sha256:".to_string() + &"1".repeat(64)).unwrap(),
10679 original_size: WireU64::new(1),
10680 },
10681 }),
10682 ));
10683 assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
10684 }
10685
10686 #[test]
10688 fn a_failed_archive_is_abandoned_and_the_run_stays_live() {
10689 let (mut runtime, archive) = agent_awaiting_page_out();
10690 let resumed = runtime.submit(&failed(
10691 "in-archive-failed",
10692 1_700_000_004_000,
10693 &archive,
10694 HostEffectFailureKind::StorageUnavailable,
10695 "the blob store is offline",
10696 ));
10697 assert_eq!(
10698 kinds(&resumed),
10699 vec![EffectKindTag::CallProvider],
10700 "DEC-5 · the archive is abandoned once; the compaction it belongs to already happened, \
10701 so the run continues degraded rather than dying on a best-effort durability effect"
10702 );
10703 assert!(
10704 observation_kinds(&runtime).contains(&"page_out_archive_failed"),
10705 "{:?}",
10706 observation_kinds(&runtime)
10707 );
10708 }
10709
10710 #[test]
10714 fn a_payload_load_outcome_is_refused_with_its_reason() {
10715 let (mut runtime, provider) = agent_awaiting_provider();
10716 let fault = runtime.reject(&resolved(
10717 "in-loaded",
10718 1_700_000_002_000,
10719 &provider,
10720 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
10721 handle_id: HandleId::new("call-1").unwrap(),
10722 payload: InlinePayload {
10723 content: "body".to_string(),
10724 digest: Digest::new("sha256:".to_string() + &"2".repeat(64)).unwrap(),
10725 original_size: WireU64::new(4),
10726 },
10727 }),
10728 ));
10729 assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
10731 }
10732
10733 #[test]
10737 fn the_failure_vocabulary_cannot_express_a_cancellation() {
10738 for kind in HostEffectFailureKind::ALL {
10739 let label = kind.as_str();
10740 assert!(
10741 !label.contains("cancel"),
10742 "cancellation is a control-plane fact and only reaches the kernel through \
10743 HostControl::Cancel; {label} would give one pending effect two meanings"
10744 );
10745 }
10746 assert_eq!(HostEffectFailureKind::ALL.len(), 6);
10747 }
10748
10749 fn agent_start_with_history(id: &str, at: u64, messages: usize) -> WireEnvelope {
10752 envelope(
10753 id,
10754 at,
10755 KernelInput::StartOperation(StartOperation {
10756 entry: RootEntry::Agent(RootAgentEntry {
10757 task: LogicalTask::new("write the research brief"),
10758 run_spec: None,
10759 }),
10760 initial_context: InitialContext {
10761 messages: (0..messages)
10762 .map(|index| super::super::root::LogicalMessage {
10763 role: if index % 2 == 0 {
10764 MessageRole::User
10765 } else {
10766 MessageRole::Assistant
10767 },
10768 content: format!(
10769 "turn {index}: a long enough body that compaction has something \
10770 to reclaim when the prompt stops fitting"
10771 ),
10772 tokens: Some(64),
10773 tool_call_id: None,
10774 })
10775 .collect(),
10776 ..InitialContext::default()
10777 },
10778 }),
10779 )
10780 }
10781
10782 fn agent_awaiting_approval() -> (Runtime, EffectId) {
10784 use crate::runtime::kernel::wire::command::{GovernancePolicy, PolicyAction, PolicyRule};
10785
10786 let mut runtime = Runtime::new();
10787 runtime.submit(&syscall_config_with(|config| {
10788 config.host_effect_support = support_with([EffectKindTag::RequestApproval]);
10789 config.governance_policy = Some(GovernancePolicy {
10790 rules: vec![PolicyRule {
10791 tool_pattern: "search".to_string(),
10792 action: PolicyAction::AskUser,
10793 }],
10794 ..GovernancePolicy::default()
10795 });
10796 }));
10797 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
10798 (runtime, sole_effect(&started).effect_id.clone())
10799 }
10800
10801 fn brief_contract() -> WireVerificationContract {
10805 WireVerificationContract {
10806 contract_id: "brief-quality-v1".to_string(),
10807 phases: vec![
10808 WireMilestonePhase {
10809 phase_id: "collect".to_string(),
10810 unlocks: vec!["search".to_string()],
10811 },
10812 WireMilestonePhase {
10813 phase_id: "write".to_string(),
10814 unlocks: vec!["debug".to_string()],
10815 },
10816 ],
10817 }
10818 }
10819
10820 fn agent_start_under_contract(id: &str, at: u64, contract_id: &str) -> WireEnvelope {
10821 envelope(
10822 id,
10823 at,
10824 KernelInput::StartOperation(StartOperation {
10825 entry: RootEntry::Agent(RootAgentEntry {
10826 task: LogicalTask::new("write the research brief"),
10827 run_spec: Some(LogicalAgentSpec {
10828 verification_contract_id: Some(contract_id.to_string()),
10829 ..LogicalAgentSpec::new("write the research brief")
10830 }),
10831 }),
10832 initial_context: InitialContext::default(),
10833 }),
10834 )
10835 }
10836
10837 fn agent_awaiting_milestone_check() -> (Runtime, EffectId) {
10842 let mut runtime = Runtime::new();
10843 runtime.submit(&syscall_config_with(|config| {
10844 config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10845 config.verification_contracts = vec![brief_contract()];
10846 }));
10847 let started = runtime.submit(&agent_start_under_contract(
10848 "in-start",
10849 1_700_000_001_000,
10850 "brief-quality-v1",
10851 ));
10852 (runtime, sole_effect(&started).effect_id.clone())
10853 }
10854
10855 fn agent_awaiting_memory_write() -> (Runtime, EffectId) {
10857 use crate::runtime::kernel::wire::syscall::{
10858 MemoryWriteProposal, RequestMemoryWriteRequest,
10859 };
10860
10861 let mut runtime = Runtime::new();
10862 runtime.submit(&syscall_config());
10863 let started = runtime.submit(&workflow_start(
10864 "in-start",
10865 1_700_000_001_000,
10866 two_node_spec(),
10867 ));
10868 runtime.submit(&spawned(
10869 "in-ack",
10870 1_700_000_002_000,
10871 &effect_id(started.step_seq),
10872 &["wf-node0"],
10873 ));
10874 let completed = runtime.submit(&child_done_with(
10875 "in-done",
10876 1_700_000_002_500,
10877 "wf-node0",
10878 "wf-node0:attempt:1",
10879 vec![SyscallRequest::RequestMemoryWrite(
10880 RequestMemoryWriteRequest {
10881 proposal: MemoryWriteProposal {
10882 name: "brief-style".to_string(),
10883 kind: SyscallMemoryKind::Project,
10884 content: "prefers numbered sections".to_string(),
10885 description: String::new(),
10886 evidence_refs: Vec::new(),
10887 },
10888 },
10889 )],
10890 ));
10891 let effect = completed
10892 .published_effects()
10893 .iter()
10894 .find(|effect| effect.tag() == EffectKindTag::PersistMemory)
10895 .expect("the child's request published a memory write")
10896 .effect_id
10897 .clone();
10898 (runtime, effect)
10899 }
10900
10901 fn agent_awaiting_page_out() -> (Runtime, EffectId) {
10904 let mut runtime = Runtime::new();
10905 runtime.submit(&syscall_config_with(|config| {
10906 config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
10907 }));
10908 let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
10909 let provider = sole_effect(&started).effect_id.clone();
10910 let compacted = runtime.submit(&provider_overflow(
10911 "in-overflow",
10912 1_700_000_002_000,
10913 &provider,
10914 ));
10915 let archive = compacted
10916 .published_effects()
10917 .iter()
10918 .find(|effect| effect.tag() == EffectKindTag::ArchivePageOut)
10919 .expect("the compaction externalised its archive")
10920 .effect_id
10921 .clone();
10922 (runtime, archive)
10923 }
10924
10925 fn workflow_with_live_child() -> (Runtime, EffectId) {
10927 let mut runtime = Runtime::new();
10928 runtime.submit(&syscall_config());
10929 let started = runtime.submit(&workflow_start(
10930 "in-start",
10931 1_700_000_001_000,
10932 two_node_spec(),
10933 ));
10934 let spawn = effect_id(started.step_seq);
10935 runtime.submit(&spawned("in-ack", 1_700_000_002_000, &spawn, &["wf-node0"]));
10936 (runtime, spawn)
10937 }
10938
10939 fn control(id: &str, at: u64, command: HostCommand) -> WireEnvelope {
10944 envelope(
10945 id,
10946 at,
10947 KernelInput::HostControl(super::super::envelope::HostControl { command }),
10948 )
10949 }
10950
10951 fn cancel_with(id: &str, at: u64, reason: CancellationReason) -> WireEnvelope {
10952 control(
10953 id,
10954 at,
10955 HostCommand::Cancel(CancelCommand {
10956 reason,
10957 pending_call_ids: Vec::new(),
10958 }),
10959 )
10960 }
10961
10962 fn cancel(id: &str, at: u64) -> WireEnvelope {
10963 cancel_with(id, at, CancellationReason::User)
10964 }
10965
10966 fn signal_delivery(
10969 id: &str,
10970 at: u64,
10971 delivery: &str,
10972 attempt: u32,
10973 signal: LogicalSignal,
10974 ) -> WireEnvelope {
10975 envelope(
10976 id,
10977 at,
10978 KernelInput::DeliverExternalEvent(DeliverExternalEvent {
10979 event: ExternalEvent::DeliverSignal(DeliverSignal {
10980 delivery_id: DeliveryId::new(delivery).unwrap(),
10981 attempt,
10982 signal,
10983 }),
10984 }),
10985 )
10986 }
10987
10988 fn logical_signal(id: &str, urgency: SignalUrgency) -> LogicalSignal {
10989 LogicalSignal {
10990 urgency: Some(urgency),
10991 ..LogicalSignal::new(SignalId::new(id).unwrap())
10992 }
10993 }
10994
10995 fn signal_config(queue_max: u32, ttl_ms: Option<u64>) -> WireEnvelope {
10998 syscall_config_with(|config| {
10999 config.signal_policy = Some(super::super::command::SignalPolicy {
11000 queue_max,
11001 ttl_ms: ttl_ms.map(WireU64::new),
11002 deadline_escalation: None,
11003 });
11004 })
11005 }
11006
11007 #[test]
11009 fn cancellation_enters_only_through_the_control_plane_and_never_as_a_failure() {
11010 let (mut runtime, provider) = agent_awaiting_provider();
11013 let failed_out = runtime.submit(&failed(
11014 "in-transport-dead",
11015 1_700_000_002_000,
11016 &provider,
11017 HostEffectFailureKind::TransportExhausted,
11018 "the vendor gave up",
11019 ));
11020 assert!(
11021 matches!(failed_out.terminal(), Some(KernelTerminal::Failed(_))),
11022 "a host effect failure is never a cancellation (§14.3)"
11023 );
11024
11025 let (mut runtime, _) = agent_awaiting_provider();
11027 let cancelled = runtime.submit(&cancel_with(
11028 "in-cancel",
11029 1_700_000_002_000,
11030 CancellationReason::Deadline,
11031 ));
11032 let Some(KernelTerminal::Cancelled(terminal)) = cancelled.terminal() else {
11033 panic!(
11034 "expected a cancelled terminal, got {:?}",
11035 cancelled.terminal()
11036 );
11037 };
11038 assert_eq!(
11039 terminal.reason,
11040 CancellationReason::Deadline,
11041 "the reason is the host's, not the loop's internal user-abort"
11042 );
11043 assert!(
11044 observation_kinds(&runtime).contains(&"operation_cancelled"),
11045 "{:?}",
11046 observation_kinds(&runtime)
11047 );
11048
11049 assert!(
11052 serde_json::from_value::<CancelCommand>(
11053 json!({ "reason": "user", "operation_id": "op-driver-1" })
11054 )
11055 .is_err(),
11056 "the envelope owns the operation id (§7.5)"
11057 );
11058 let (mut runtime, _) = agent_awaiting_provider();
11059 let mut foreign = cancel("in-foreign", 1_700_000_002_000);
11060 foreign.operation_id = OperationId::new("op-someone-else").unwrap();
11061 assert_eq!(
11062 runtime.reject(&foreign).code,
11063 KernelFaultCode::OperationMismatch
11064 );
11065 }
11066
11067 #[test]
11069 fn cancellation_settles_every_downstream_wait_before_it_commits_the_root_terminal() {
11070 let (mut runtime, _) = workflow_with_live_child();
11071 assert!(
11072 runtime.driver.attempts.contains_key("wf-node0"),
11073 "the arc starts with a live child attempt"
11074 );
11075
11076 let cancelled = runtime.submit(&cancel("in-cancel", 1_700_000_003_000));
11077
11078 assert_eq!(
11080 runtime
11081 .driver
11082 .engine()
11083 .and_then(|engine| engine.task_lifecycle("wf-node0"))
11084 .map(TaskLifecycle::is_terminal),
11085 Some(true),
11086 "a running child is settled by the same step that cancels its parent"
11087 );
11088 assert!(
11089 runtime.driver.attempts.is_empty(),
11090 "a settled attempt is spent, so nothing downstream can be resumed by a late completion"
11091 );
11092 assert_eq!(
11093 runtime.pending_effect_kinds(),
11094 Vec::<EffectKindTag>::new(),
11095 "§11.1 · the cancelling step leaves nothing waiting on the host"
11096 );
11097
11098 assert!(matches!(
11100 cancelled.terminal(),
11101 Some(KernelTerminal::Cancelled(_))
11102 ));
11103 assert!(
11104 cancelled.published_effects().is_empty(),
11105 "§7.12 · effects or a terminal, never both"
11106 );
11107
11108 assert_eq!(
11110 runtime
11111 .reject(&child_done(
11112 "in-late-done",
11113 1_700_000_004_000,
11114 "wf-node0",
11115 "finished anyway"
11116 ))
11117 .code,
11118 KernelFaultCode::InvalidLifecycle,
11119 );
11120 }
11121
11122 #[test]
11124 fn a_second_cancellation_commits_no_second_terminal() {
11125 let (mut runtime, _) = agent_awaiting_provider();
11126 let first = runtime.submit(&cancel("in-cancel", 1_700_000_002_000));
11127 let head = runtime.tx.head().map(|head| head.step_seq);
11128
11129 let replay = runtime.prepare(&cancel("in-cancel-again", 1_700_000_003_000));
11132 assert_eq!(replay.step_seq(), Some(first.step_seq));
11133 assert!(
11134 replay.record().unwrap().record_digest() == first.record.record_digest(),
11135 "the replay names the existing record"
11136 );
11137 assert_eq!(
11138 runtime.tx.head().map(|head| head.step_seq),
11139 head,
11140 "a replay moves no head"
11141 );
11142
11143 assert_eq!(
11145 runtime
11146 .reject(&cancel_with(
11147 "in-cancel-other",
11148 1_700_000_003_000,
11149 CancellationReason::LeaseLost
11150 ))
11151 .code,
11152 KernelFaultCode::DuplicateInputConflict,
11153 );
11154 }
11155
11156 #[test]
11158 fn a_terminal_refuses_every_state_changing_input_including_signal_delivery() {
11159 let (mut runtime, provider) = agent_awaiting_provider();
11160 runtime.submit(&cancel("in-cancel", 1_700_000_002_000));
11161
11162 let head = runtime.tx.head().map(|head| head.step_seq);
11163 let queue_before = signal_queue_depth(&runtime);
11164 let journal_len = runtime.journal.len();
11165
11166 for envelope in [
11167 signal_delivery(
11168 "in-late-signal",
11169 1_700_000_003_000,
11170 "delivery-late",
11171 1,
11172 logical_signal("sig-late", SignalUrgency::Critical),
11173 ),
11174 child_done("in-late-child", 1_700_000_003_000, "wf-node0", "done"),
11175 provider_answer("in-late-answer", 1_700_000_003_000, &provider, "too late"),
11176 agent_start("in-restart", 1_700_000_003_000),
11177 control(
11178 "in-late-compact",
11179 1_700_000_003_000,
11180 HostCommand::ForceCompact(super::super::command::ForceCompactCommand {}),
11181 ),
11182 control(
11183 "in-late-task",
11184 1_700_000_003_000,
11185 HostCommand::UpdateTask(UpdateTaskCommand {
11186 update: WireTaskUpdate {
11187 progress: Some("still going".to_string()),
11188 ..WireTaskUpdate::default()
11189 },
11190 }),
11191 ),
11192 ] {
11193 let input_id = envelope.input_id.clone();
11194 assert_eq!(
11195 runtime.reject(&envelope).code,
11196 KernelFaultCode::InvalidLifecycle,
11197 "{input_id} must be refused after the terminal",
11198 );
11199 }
11200
11201 assert_eq!(
11202 runtime.tx.head().map(|head| head.step_seq),
11203 head,
11204 "no step sequence advanced"
11205 );
11206 assert_eq!(runtime.journal.len(), journal_len, "nothing was journaled");
11207 assert_eq!(
11208 signal_queue_depth(&runtime),
11209 queue_before,
11210 "a refused signal never reaches the queue"
11211 );
11212 assert!(
11213 runtime.driver.poison().is_none(),
11214 "a typed rejection is not a driver failure"
11215 );
11216 }
11217
11218 fn signal_queue_depth(runtime: &Runtime) -> usize {
11219 runtime
11220 .driver
11221 .engine()
11222 .map(LoopStateMachine::signal_queue_depth)
11223 .unwrap_or(0)
11224 }
11225
11226 #[test]
11228 fn delivery_identity_and_signal_identity_are_two_separate_facts() {
11229 let mut runtime = Runtime::new();
11230 runtime.submit(&signal_config(8, None));
11231 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11232
11233 let first = signal_delivery(
11234 "in-sig-1",
11235 1_700_000_002_000,
11236 "delivery-a",
11237 1,
11238 LogicalSignal {
11239 dedupe_key: Some("nightly".to_string()),
11240 ..logical_signal("sig-nightly", SignalUrgency::Normal)
11241 },
11242 );
11243 runtime.submit(&first);
11244 assert_eq!(
11245 dispositions(&runtime),
11246 vec![(
11247 "queue".to_string(),
11248 "sig-nightly".to_string(),
11249 "delivery-a".to_string(),
11250 1
11251 )],
11252 "the audit fact names the caller's own signal id, never a minted one"
11253 );
11254
11255 let depth = signal_queue_depth(&runtime);
11258 let replay = runtime.prepare(&first);
11259 assert!(matches!(
11260 replay,
11261 super::super::fault::KernelPreparation::Replayed(_)
11262 ));
11263 assert_eq!(signal_queue_depth(&runtime), depth);
11264
11265 runtime.submit(&signal_delivery(
11268 "in-sig-2",
11269 1_700_000_003_000,
11270 "delivery-b",
11271 2,
11272 LogicalSignal {
11273 dedupe_key: Some("nightly".to_string()),
11274 ..logical_signal("sig-nightly", SignalUrgency::Normal)
11275 },
11276 ));
11277 assert_eq!(
11278 dispositions(&runtime),
11279 vec![(
11280 "ignore".to_string(),
11281 "sig-nightly".to_string(),
11282 "delivery-b".to_string(),
11283 2
11284 )],
11285 "a redelivery is recognisable as the same signal and a different delivery"
11286 );
11287 assert_eq!(
11288 signal_queue_depth(&runtime),
11289 depth,
11290 "and it does not queue a second copy"
11291 );
11292
11293 assert_eq!(
11295 runtime
11296 .reject(&signal_delivery(
11297 "in-sig-0",
11298 1_700_000_004_000,
11299 "delivery-c",
11300 0,
11301 logical_signal("sig-other", SignalUrgency::Normal),
11302 ))
11303 .code,
11304 KernelFaultCode::MalformedEnvelope,
11305 );
11306 }
11307
11308 #[test]
11310 fn signal_ttl_is_measured_from_the_accepted_envelope_time() {
11311 let mut runtime = Runtime::new();
11312 runtime.submit(&signal_config(8, Some(60_000)));
11313 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11314
11315 runtime.submit(&signal_delivery(
11318 "in-sig-1",
11319 1_700_000_002_000,
11320 "delivery-a",
11321 1,
11322 LogicalSignal {
11323 source_timestamp_ms: Some(WireU64::new(1_600_000_000_000)),
11324 ..logical_signal("sig-stale-source", SignalUrgency::Normal)
11325 },
11326 ));
11327 assert_eq!(
11328 dispositions(&runtime)
11329 .iter()
11330 .map(|(disposition, ..)| disposition.clone())
11331 .collect::<Vec<_>>(),
11332 vec!["queue".to_string()],
11333 "admission uses the accepted envelope time; the source timestamp is metadata"
11334 );
11335 assert_eq!(signal_queue_depth(&runtime), 1);
11336
11337 runtime.submit(&signal_delivery(
11340 "in-sig-2",
11341 1_700_000_200_000,
11342 "delivery-b",
11343 1,
11344 logical_signal("sig-fresh", SignalUrgency::Normal),
11345 ));
11346 assert!(
11347 observation_kinds(&runtime).contains(&"signal_expired"),
11348 "{:?}",
11349 observation_kinds(&runtime)
11350 );
11351 assert_eq!(
11352 signal_queue_depth(&runtime),
11353 1,
11354 "the stale signal left, the fresh one stayed"
11355 );
11356 }
11357
11358 #[test]
11360 fn a_signal_addresses_the_operation_or_one_of_its_own_tasks() {
11361 let (mut runtime, _) = workflow_with_live_child();
11362
11363 runtime.submit(&signal_delivery(
11365 "in-sig-op",
11366 1_700_000_003_000,
11367 "delivery-a",
11368 1,
11369 logical_signal("sig-op", SignalUrgency::Normal),
11370 ));
11371 assert_eq!(dispositions(&runtime).len(), 1);
11372
11373 runtime.submit(&signal_delivery(
11375 "in-sig-task",
11376 1_700_000_004_000,
11377 "delivery-b",
11378 1,
11379 LogicalSignal {
11380 target: SignalTarget::Task(super::super::event::TaskTarget {
11381 task_id: TaskId::new("wf-node0").unwrap(),
11382 }),
11383 ..logical_signal("sig-task", SignalUrgency::Normal)
11384 },
11385 ));
11386 assert_eq!(dispositions(&runtime).len(), 1);
11387
11388 assert_eq!(
11390 runtime
11391 .reject(&signal_delivery(
11392 "in-sig-ghost",
11393 1_700_000_005_000,
11394 "delivery-c",
11395 1,
11396 LogicalSignal {
11397 target: SignalTarget::Task(super::super::event::TaskTarget {
11398 task_id: TaskId::new("ghost").unwrap(),
11399 }),
11400 ..logical_signal("sig-ghost", SignalUrgency::Normal)
11401 },
11402 ))
11403 .code,
11404 KernelFaultCode::InvalidAuthority,
11405 );
11406
11407 assert!(
11409 serde_json::from_value::<SignalTarget>(
11410 json!({ "kind": "task", "task_id": "wf-node0", "session_id": "sess-1" })
11411 )
11412 .is_err(),
11413 "host session identity does not enter the event (§7.7)"
11414 );
11415 }
11416
11417 #[test]
11419 fn a_full_signal_queue_drops_by_policy_and_leaves_an_audit_fact() {
11420 let mut runtime = Runtime::new();
11421 runtime.submit(&signal_config(1, None));
11422 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11423
11424 runtime.submit(&signal_delivery(
11425 "in-sig-1",
11426 1_700_000_002_000,
11427 "delivery-a",
11428 1,
11429 logical_signal("sig-1", SignalUrgency::Normal),
11430 ));
11431 runtime.submit(&signal_delivery(
11432 "in-sig-2",
11433 1_700_000_003_000,
11434 "delivery-b",
11435 1,
11436 logical_signal("sig-2", SignalUrgency::Normal),
11437 ));
11438 assert_eq!(
11439 dispositions(&runtime)
11440 .iter()
11441 .map(|(disposition, ..)| disposition.clone())
11442 .collect::<Vec<_>>(),
11443 vec!["dropped".to_string()],
11444 "the configured capacity decides, and the loss is an audit fact"
11445 );
11446 assert_eq!(signal_queue_depth(&runtime), 1);
11447 }
11448
11449 #[test]
11451 fn a_signal_disposition_is_a_fact_and_only_a_preemption_asks_the_host_for_anything() {
11452 let mut runtime = Runtime::new();
11454 runtime.submit(&signal_config(8, None));
11455 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11456 let queued = runtime.submit(&signal_delivery(
11457 "in-sig-normal",
11458 1_700_000_002_000,
11459 "delivery-a",
11460 1,
11461 logical_signal("sig-normal", SignalUrgency::Normal),
11462 ));
11463 assert!(
11464 queued.published_effects().is_empty(),
11465 "queueing is a fact; it asks the host for nothing"
11466 );
11467 assert!(observation_kinds(&runtime).contains(&"signal_delivery_disposed"));
11468
11469 let (mut runtime, _) = workflow_with_live_child();
11471 let interrupted = runtime.submit(&signal_delivery(
11472 "in-sig-critical",
11473 1_700_000_003_000,
11474 "delivery-b",
11475 1,
11476 logical_signal("sig-critical", SignalUrgency::Critical),
11477 ));
11478 assert_eq!(kinds(&interrupted), vec![EffectKindTag::PreemptTasks]);
11479 assert!(
11480 !observation_kinds(&runtime).contains(&"agent_preempted"),
11481 "the preemption is requested here, not committed: {:?}",
11482 observation_kinds(&runtime)
11483 );
11484
11485 let preempt = effect_id(interrupted.step_seq);
11487 runtime.submit(&resolved(
11488 "in-preempted",
11489 1_700_000_004_000,
11490 &preempt,
11491 EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
11492 attempts: vec![super::super::effect::TaskPreemptOutcome {
11493 task_id: TaskId::new("wf-node0").unwrap(),
11494 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
11495 outcome: super::super::effect::TaskPreemptStatus::Preempted(
11496 super::super::effect::TaskPreempted {},
11497 ),
11498 }],
11499 }),
11500 ));
11501 assert!(
11502 observation_kinds(&runtime).contains(&"agent_preempted"),
11503 "{:?}",
11504 observation_kinds(&runtime)
11505 );
11506 }
11507
11508 #[test]
11509 fn an_operation_that_can_be_interrupted_is_one_that_can_stop_its_children() {
11510 let mut runtime = Runtime::new();
11516 let fault = runtime.reject(&syscall_config_with(|config| {
11517 config.host_effect_support = HostEffectSupport::new([
11518 EffectKindTag::CallProvider,
11519 EffectKindTag::ExecuteTools,
11520 EffectKindTag::LoadPayload,
11521 EffectKindTag::SpawnTasks,
11522 EffectKindTag::PersistMemory,
11523 EffectKindTag::QueryMemory,
11524 ]);
11525 }));
11526 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
11527 assert!(fault.message.contains("preempt_tasks"), "{fault:?}");
11528 }
11529
11530 fn escalating_signal_config(queue_max: u32) -> WireEnvelope {
11534 syscall_config_with(|config| {
11535 config.signal_policy = Some(super::super::command::SignalPolicy {
11536 queue_max,
11537 ttl_ms: None,
11538 deadline_escalation: Some(true),
11539 });
11540 })
11541 }
11542
11543 fn signal_escalating_after(id: &str, urgency: SignalUrgency, after_ms: u64) -> LogicalSignal {
11544 LogicalSignal {
11545 escalate_after_ms: Some(WireU64::new(after_ms)),
11546 ..logical_signal(id, urgency)
11547 }
11548 }
11549
11550 #[test]
11551 fn a_due_escalation_raises_urgency_one_tier_and_is_anchored_to_the_accepted_time() {
11552 let mut runtime = Runtime::new();
11555 runtime.submit(&escalating_signal_config(8));
11556 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11557 runtime.submit(&signal_delivery(
11558 "in-sig-due",
11559 1_700_000_002_000,
11560 "delivery-a",
11561 1,
11562 signal_escalating_after("sig-due", SignalUrgency::Low, 0),
11563 ));
11564 assert_eq!(
11565 dispositions(&runtime)
11566 .iter()
11567 .map(|(disposition, ..)| disposition.clone())
11568 .collect::<Vec<_>>(),
11569 vec!["queue".to_string()],
11570 "a due deadline escalated low → normal"
11571 );
11572
11573 let mut runtime = Runtime::new();
11577 runtime.submit(&escalating_signal_config(8));
11578 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11579 runtime.submit(&signal_delivery(
11580 "in-sig-waiting",
11581 1_700_000_002_000,
11582 "delivery-a",
11583 1,
11584 signal_escalating_after("sig-waiting", SignalUrgency::Low, 60_000),
11585 ));
11586 assert_eq!(
11587 dispositions(&runtime)
11588 .iter()
11589 .map(|(disposition, ..)| disposition.clone())
11590 .collect::<Vec<_>>(),
11591 vec!["observe".to_string()],
11592 "a deadline that has not come due changes nothing"
11593 );
11594 }
11595
11596 #[test]
11597 fn escalation_is_inert_unless_the_operation_asked_for_it() {
11598 let mut runtime = Runtime::new();
11601 runtime.submit(&signal_config(8, None));
11602 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11603 runtime.submit(&signal_delivery(
11604 "in-sig",
11605 1_700_000_002_000,
11606 "delivery-a",
11607 1,
11608 signal_escalating_after("sig-inert", SignalUrgency::Low, 0),
11609 ));
11610 assert_eq!(
11611 dispositions(&runtime)
11612 .iter()
11613 .map(|(disposition, ..)| disposition.clone())
11614 .collect::<Vec<_>>(),
11615 vec!["observe".to_string()],
11616 "escalation without the policy is inert"
11617 );
11618 }
11619
11620 #[test]
11621 fn an_escalation_that_reaches_critical_takes_the_whole_interrupt_arc() {
11622 let mut runtime = Runtime::new();
11628 runtime.submit(&escalating_signal_config(8));
11629 let started = runtime.submit(&workflow_start(
11630 "in-start",
11631 1_700_000_001_000,
11632 two_node_spec(),
11633 ));
11634 runtime.submit(&spawned(
11635 "in-ack",
11636 1_700_000_002_000,
11637 &effect_id(started.step_seq),
11638 &["wf-node0"],
11639 ));
11640
11641 let interrupted = runtime.submit(&signal_delivery(
11642 "in-sig-escalated",
11643 1_700_000_003_000,
11644 "delivery-a",
11645 1,
11646 signal_escalating_after("sig-escalated", SignalUrgency::High, 0),
11647 ));
11648 assert_eq!(
11649 kinds(&interrupted),
11650 vec![EffectKindTag::PreemptTasks],
11651 "high + due deadline = critical, and critical while busy preempts"
11652 );
11653
11654 let mut runtime = Runtime::new();
11656 runtime.submit(&escalating_signal_config(8));
11657 let started = runtime.submit(&workflow_start(
11658 "in-start",
11659 1_700_000_001_000,
11660 two_node_spec(),
11661 ));
11662 runtime.submit(&spawned(
11663 "in-ack",
11664 1_700_000_002_000,
11665 &effect_id(started.step_seq),
11666 &["wf-node0"],
11667 ));
11668 let soft = runtime.submit(&signal_delivery(
11669 "in-sig-plain",
11670 1_700_000_003_000,
11671 "delivery-a",
11672 1,
11673 logical_signal("sig-plain", SignalUrgency::High),
11674 ));
11675 assert!(
11676 soft.published_effects().is_empty(),
11677 "an unescalated high signal waits for the next boundary"
11678 );
11679 }
11680
11681 #[test]
11682 fn a_signal_carries_a_duration_not_a_deadline() {
11683 let signal = signal_escalating_after("sig", SignalUrgency::Normal, 30_000);
11687 let value = serde_json::to_value(&signal).unwrap();
11688 assert_eq!(value["escalate_after_ms"], json!("30000"));
11689 for banned in ["deadline_ms", "escalate_at_ms", "expires_at_ms", "now_ms"] {
11690 assert!(
11691 value.get(banned).is_none(),
11692 "a signal must not carry {banned}"
11693 );
11694 let mut with_instant = value.clone();
11695 with_instant
11696 .as_object_mut()
11697 .unwrap()
11698 .insert(banned.to_string(), json!("1700000000000"));
11699 assert!(
11700 serde_json::from_value::<LogicalSignal>(with_instant).is_err(),
11701 "{banned} must not decode"
11702 );
11703 }
11704
11705 let plain = serde_json::to_value(logical_signal("sig", SignalUrgency::Normal)).unwrap();
11707 assert!(plain.get("escalate_after_ms").is_none());
11708 }
11709
11710 #[test]
11711 fn an_urgent_signal_never_publishes_a_second_provider_request() {
11712 let (mut runtime, provider) = agent_awaiting_provider();
11715 let interrupted = runtime.submit(&signal_delivery(
11716 "in-sig-critical",
11717 1_700_000_002_000,
11718 "delivery-a",
11719 1,
11720 logical_signal("sig-critical", SignalUrgency::Critical),
11721 ));
11722 assert!(
11723 interrupted.published_effects().is_empty(),
11724 "a second provider call would be refused by §15.3, so none is planned"
11725 );
11726 assert_eq!(
11727 dispositions(&runtime)
11728 .iter()
11729 .map(|(disposition, ..)| disposition.clone())
11730 .collect::<Vec<_>>(),
11731 vec!["interrupt".to_string()],
11732 "the disposition reports what actually happened"
11733 );
11734
11735 let answered = runtime.submit(&provider_result(
11737 "in-answer",
11738 1_700_000_003_000,
11739 &provider,
11740 vec![tool_call("call-1", "search", json!({"q": "now what"}))],
11741 ));
11742 assert_eq!(kinds(&answered), vec![EffectKindTag::ExecuteTools]);
11743 }
11744
11745 #[test]
11750 fn a_host_task_update_and_a_model_task_update_share_a_payload_but_not_an_authority() {
11751 let (mut runtime, provider) = agent_awaiting_provider();
11752 let committed = runtime.submit(&control(
11753 "in-host-plan",
11754 1_700_000_002_000,
11755 HostCommand::UpdateTask(UpdateTaskCommand {
11756 update: WireTaskUpdate {
11757 plan: Some(vec!["collect".to_string(), "write".to_string()]),
11758 progress: Some("host set the plan".to_string()),
11759 ..WireTaskUpdate::default()
11760 },
11761 }),
11762 ));
11763 assert!(
11764 committed.published_effects().is_empty() && committed.terminal().is_none(),
11765 "a control command changes kernel state and publishes nothing"
11766 );
11767 assert_eq!(
11768 runtime
11769 .driver
11770 .engine()
11771 .map(|engine| engine.ctx.partitions.task_state.progress.clone()),
11772 Some("host set the plan".to_string()),
11773 );
11774
11775 let acted = runtime.submit(&provider_result(
11778 "in-model-plan",
11779 1_700_000_003_000,
11780 &provider,
11781 vec![tool_call(
11782 "call-1",
11783 "update_plan",
11784 json!({ "progress": "model set the plan" }),
11785 )],
11786 ));
11787 assert_eq!(kinds(&acted), vec![EffectKindTag::CallProvider]);
11788 assert_eq!(
11789 runtime
11790 .driver
11791 .engine()
11792 .map(|engine| engine.ctx.partitions.task_state.progress.clone()),
11793 Some("model set the plan".to_string()),
11794 );
11795 }
11796
11797 #[test]
11798 fn seeded_and_mutated_knowledge_enter_the_same_partition() {
11799 let (mut runtime, _) = agent_awaiting_provider();
11800 runtime.submit(&control(
11801 "in-seed",
11802 1_700_000_002_000,
11803 HostCommand::SeedKnowledge(SeedKnowledgeCommand {
11804 entries: vec![super::super::root::KnowledgeEntry {
11805 content: "the house style forbids bullet lists".to_string(),
11806 key: Some("style".to_string()),
11807 tokens: Some(9),
11808 pinned: true,
11809 }],
11810 }),
11811 ));
11812 assert!(
11813 knowledge_text(&runtime)
11814 .iter()
11815 .any(|text| text.contains("house style")),
11816 "{:?}",
11817 knowledge_text(&runtime)
11818 );
11819
11820 let committed = runtime.submit(&control(
11822 "in-knowledge",
11823 1_700_000_003_000,
11824 HostCommand::ApplyKnowledgeMutation(ApplyKnowledgeMutationCommand {
11825 mutation: super::super::command::KnowledgeMutation {
11826 upsert: vec![super::super::root::KnowledgeEntry {
11827 content: "cite at least three sources".to_string(),
11828 key: Some("sources".to_string()),
11829 tokens: Some(6),
11830 pinned: false,
11831 }],
11832 remove: vec!["style".to_string(), "never-seen".to_string()],
11833 },
11834 }),
11835 ));
11836 assert!(committed.published_effects().is_empty());
11837 assert!(
11838 knowledge_text(&runtime)
11839 .iter()
11840 .any(|text| text.contains("three sources")),
11841 "{:?}",
11842 knowledge_text(&runtime)
11843 );
11844 }
11845
11846 fn knowledge_text(runtime: &Runtime) -> Vec<String> {
11847 runtime
11848 .driver
11849 .engine()
11850 .map(|engine| {
11851 engine
11852 .ctx
11853 .partitions
11854 .knowledge
11855 .messages()
11856 .map(message_text)
11857 .collect()
11858 })
11859 .unwrap_or_default()
11860 }
11861
11862 #[test]
11863 fn a_capability_patch_mounts_and_unmounts_in_one_step() {
11864 use super::super::root::{CapabilityGrant, CapabilityKind as WireCapabilityKind};
11865
11866 let (mut runtime, _) = agent_awaiting_provider();
11867 runtime.submit(&control(
11868 "in-mount",
11869 1_700_000_002_000,
11870 HostCommand::ApplyCapabilityPatch(ApplyCapabilityPatchCommand {
11871 patch: super::super::command::CapabilityPatch {
11872 mount: vec![CapabilityGrant {
11873 kind: WireCapabilityKind::McpServer,
11874 id: "github".to_string(),
11875 description: Some("issue and PR access".to_string()),
11876 }],
11877 unmount: Vec::new(),
11878 },
11879 }),
11880 ));
11881 assert!(observation_kinds(&runtime).contains(&"capability_changed"));
11882 assert!(
11883 runtime
11884 .driver
11885 .engine()
11886 .unwrap()
11887 .ctx
11888 .capabilities
11889 .capabilities()
11890 .iter()
11891 .any(|capability| capability.id == "github")
11892 );
11893
11894 runtime.submit(&control(
11896 "in-unmount",
11897 1_700_000_003_000,
11898 HostCommand::ApplyCapabilityPatch(ApplyCapabilityPatchCommand {
11899 patch: super::super::command::CapabilityPatch {
11900 mount: Vec::new(),
11901 unmount: vec![
11902 super::super::root::CapabilityRef {
11903 kind: WireCapabilityKind::McpServer,
11904 id: "github".to_string(),
11905 },
11906 super::super::root::CapabilityRef {
11907 kind: WireCapabilityKind::McpServer,
11908 id: "never-mounted".to_string(),
11909 },
11910 ],
11911 },
11912 }),
11913 ));
11914 assert!(
11915 !runtime
11916 .driver
11917 .engine()
11918 .unwrap()
11919 .ctx
11920 .capabilities
11921 .capabilities()
11922 .iter()
11923 .any(|capability| capability.id == "github")
11924 );
11925 }
11926
11927 #[test]
11928 fn a_skill_swap_is_atomic_and_refuses_a_name_outside_the_catalog() {
11929 let (mut runtime, _) = agent_awaiting_provider();
11930 let before = runtime.driver.engine().unwrap().ctx.active_skills.len();
11931
11932 assert_eq!(
11934 runtime
11935 .reject(&control(
11936 "in-bad-skill",
11937 1_700_000_002_000,
11938 HostCommand::ApplySkillActivation(ApplySkillActivationCommand {
11939 activate: vec![
11940 super::super::command::SkillActivation {
11941 name: "debug".to_string(),
11942 lease_turns: None,
11943 },
11944 super::super::command::SkillActivation {
11945 name: "invented".to_string(),
11946 lease_turns: None,
11947 },
11948 ],
11949 deactivate: Vec::new(),
11950 }),
11951 ))
11952 .code,
11953 KernelFaultCode::InvalidConfig,
11954 );
11955 assert_eq!(
11956 runtime.driver.engine().unwrap().ctx.active_skills.len(),
11957 before,
11958 "a refused swap left nothing behind"
11959 );
11960
11961 runtime.submit(&control(
11962 "in-skill",
11963 1_700_000_002_000,
11964 HostCommand::ApplySkillActivation(ApplySkillActivationCommand {
11965 activate: vec![super::super::command::SkillActivation {
11966 name: "debug".to_string(),
11967 lease_turns: Some(2),
11968 }],
11969 deactivate: Vec::new(),
11970 }),
11971 ));
11972 assert!(
11973 runtime
11974 .driver
11975 .engine()
11976 .unwrap()
11977 .ctx
11978 .active_skills
11979 .iter()
11980 .any(|(skill, _)| skill == "debug")
11981 );
11982 }
11983
11984 #[test]
11985 fn a_live_policy_patch_is_revision_guarded_and_takes_effect() {
11986 let mut runtime = Runtime::new();
11987 runtime.submit(&signal_config(8, None));
11988 runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11989
11990 let stale = control(
11992 "in-stale-policy",
11993 1_700_000_002_000,
11994 HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
11995 expected_revision: WireU64::new(7),
11996 patch: super::super::command::LivePolicyPatch::ReplaceSignalPolicy(
11997 super::super::command::ReplaceSignalPolicy {
11998 policy: super::super::command::SignalPolicy {
11999 queue_max: 1,
12000 ttl_ms: None,
12001 deadline_escalation: None,
12002 },
12003 },
12004 ),
12005 }),
12006 );
12007 let fault = runtime.reject(&stale);
12008 assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
12009 assert!(fault.message.contains("revision mismatch"), "{fault:?}");
12010 assert_eq!(
12011 runtime
12012 .driver
12013 .policy
12014 .as_ref()
12015 .map(LivePolicyState::revision),
12016 Some(WireU64::ZERO),
12017 );
12018
12019 let widening = control(
12021 "in-widen",
12022 1_700_000_002_000,
12023 HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
12024 expected_revision: WireU64::ZERO,
12025 patch: super::super::command::LivePolicyPatch::TightenResourceQuota(
12026 super::super::command::TightenResourceQuota {
12027 max_workflow_nodes: Some(999),
12028 ..Default::default()
12029 },
12030 ),
12031 }),
12032 );
12033 assert!(
12034 runtime
12035 .reject(&widening)
12036 .message
12037 .contains("may only tighten")
12038 );
12039
12040 runtime.submit(&control(
12042 "in-policy",
12043 1_700_000_002_000,
12044 HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
12045 expected_revision: WireU64::ZERO,
12046 patch: super::super::command::LivePolicyPatch::ReplaceSignalPolicy(
12047 super::super::command::ReplaceSignalPolicy {
12048 policy: super::super::command::SignalPolicy {
12049 queue_max: 1,
12050 ttl_ms: None,
12051 deadline_escalation: None,
12052 },
12053 },
12054 ),
12055 }),
12056 ));
12057 assert_eq!(
12058 runtime
12059 .driver
12060 .policy
12061 .as_ref()
12062 .map(LivePolicyState::revision),
12063 Some(WireU64::new(1)),
12064 );
12065 assert!(
12066 observation_kinds(&runtime).contains(&"live_policy_changed"),
12067 "{:?}",
12068 observation_kinds(&runtime)
12069 );
12070
12071 runtime.submit(&signal_delivery(
12073 "in-sig-1",
12074 1_700_000_003_000,
12075 "delivery-a",
12076 1,
12077 logical_signal("sig-1", SignalUrgency::Normal),
12078 ));
12079 runtime.submit(&signal_delivery(
12080 "in-sig-2",
12081 1_700_000_004_000,
12082 "delivery-b",
12083 1,
12084 logical_signal("sig-2", SignalUrgency::Normal),
12085 ));
12086 assert_eq!(
12087 dispositions(&runtime)
12088 .iter()
12089 .map(|(disposition, ..)| disposition.clone())
12090 .collect::<Vec<_>>(),
12091 vec!["dropped".to_string()],
12092 "the patched queue_max is what the next admission decision reads"
12093 );
12094 }
12095
12096 #[test]
12097 fn an_absolute_deadline_becomes_the_wall_budget_axis() {
12098 let (mut runtime, provider) = agent_awaiting_provider();
12099 runtime.submit(&control(
12101 "in-deadline",
12102 1_700_000_002_000,
12103 HostCommand::UpdateDeadline(UpdateDeadlineCommand {
12104 deadline_ms: Some(WireU64::new(1_700_000_001_500)),
12105 }),
12106 ));
12107
12108 let acted = runtime.submit(&provider_result(
12111 "in-acted",
12112 1_700_000_003_000,
12113 &provider,
12114 vec![tool_call("call-1", "search", json!({ "q": "sources" }))],
12115 ));
12116 let results = runtime.submit(&tools_resolved(
12117 "in-results",
12118 1_700_000_004_000,
12119 &effect_id(acted.step_seq),
12120 &[("call-1", "three sources", false)],
12121 ));
12122 assert_eq!(
12123 kinds(&results),
12124 vec![EffectKindTag::CallProvider],
12125 "an exhausted budget still buys exactly one bounded final turn"
12126 );
12127
12128 let ended = runtime.submit(&provider_answer(
12129 "in-answer",
12130 1_700_000_005_000,
12131 &effect_id(results.step_seq),
12132 "half a thought",
12133 ));
12134 let Some(KernelTerminal::Agent(agent)) = ended.terminal() else {
12135 panic!("expected the deadline to end the operation, got {ended:?}");
12136 };
12137 assert_eq!(agent.result.termination, WireTermination::Deadline);
12138 }
12139
12140 #[test]
12141 fn a_forced_compaction_publishes_the_archive_it_produced() {
12142 let mut runtime = Runtime::new();
12143 runtime.submit(&syscall_config_with(|config| {
12144 config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
12145 }));
12146 runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
12147
12148 let compacted = runtime.submit(&control(
12149 "in-compact",
12150 1_700_000_002_000,
12151 HostCommand::ForceCompact(super::super::command::ForceCompactCommand {}),
12152 ));
12153 assert_eq!(kinds(&compacted), vec![EffectKindTag::ArchivePageOut]);
12154 assert!(observation_kinds(&runtime).contains(&"compressed"));
12155 assert!(
12156 compacted
12157 .step
12158 .observations
12159 .iter()
12160 .any(|observation| matches!(observation, KernelObservation::Compressed { .. })),
12161 "the committed planned step is the host publication channel for observations"
12162 );
12163 }
12164
12165 fn fixture_dir() -> PathBuf {
12170 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
12171 }
12172
12173 fn golden(name: &str, produced: &Value) -> Value {
12174 let path = fixture_dir().join(name);
12175 if std::env::var("BLESS_KERNEL_RECORD_FIXTURES").as_deref() == Ok("1") {
12176 let mut text = serde_json::to_string_pretty(produced).unwrap();
12177 text.push('\n');
12178 fs::write(&path, text).unwrap_or_else(|e| panic!("cannot bless {name}: {e}"));
12179 return produced.clone();
12180 }
12181 let raw = fs::read_to_string(&path).unwrap_or_else(|e| {
12182 panic!("missing golden {name} ({e}); re-bless with BLESS_KERNEL_RECORD_FIXTURES=1")
12183 });
12184 serde_json::from_str(&raw).unwrap_or_else(|e| panic!("{name} is not JSON: {e}"))
12185 }
12186
12187 fn link(envelope: &WireEnvelope, committed: &CommittedTransition<PlannedStep>) -> Value {
12188 json!({
12189 "envelope": serde_json::to_value(envelope).unwrap(),
12190 "step": serde_json::to_value(&committed.step).unwrap(),
12191 "record": serde_json::to_value(&committed.record).unwrap(),
12192 })
12193 }
12194
12195 #[test]
12196 fn golden_lifecycle_agent_root() {
12197 let mut runtime = Runtime::new();
12198 let configure_envelope = configure();
12199 let start_envelope = agent_start("in-start", 1_700_000_001_000);
12200 let genesis = runtime.submit(&configure_envelope);
12201 let started = runtime.submit(&start_envelope);
12202
12203 let produced = json!({
12204 "description":
12205 "Configure → atomic agent root start (spec 6, 7.4). Two accepted inputs reach the \
12206 first provider call: the genesis record freezes the resolved configuration, and \
12207 the start record's step carries the immutable root kind, the initial execution \
12208 focus and the one CallProvider effect the transition published.",
12209 "genesis_digest": genesis.record.record_digest().as_str(),
12210 "head_digest": started.record.record_digest().as_str(),
12211 "links": [
12212 link(&configure_envelope, &genesis),
12213 link(&start_envelope, &started),
12214 ],
12215 });
12216
12217 let expected = golden("golden_lifecycle_agent_root.json", &produced);
12218 assert_eq!(produced, expected, "the agent root lifecycle drifted");
12219 assert_eq!(expected["links"][1]["step"]["root_kind"], json!("agent"));
12220 assert_eq!(
12221 expected["links"][1]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12222 json!("call_provider"),
12223 );
12224 }
12225
12226 #[test]
12227 fn golden_lifecycle_agent_full_turn() {
12228 let mut runtime = Runtime::new();
12229 let configure_envelope = syscall_config();
12230 let start_envelope = agent_start("in-start", 1_700_000_001_000);
12231 let genesis = runtime.submit(&configure_envelope);
12232 let started = runtime.submit(&start_envelope);
12233
12234 let acted_envelope = provider_result(
12235 "in-acted",
12236 1_700_000_002_000,
12237 &effect_id(started.step_seq),
12238 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12239 );
12240 let acted = runtime.submit(&acted_envelope);
12241 let results_envelope = tools_resolved(
12242 "in-results",
12243 1_700_000_003_000,
12244 &effect_id(acted.step_seq),
12245 &[("call-1", "three sources found", false)],
12246 );
12247 let results = runtime.submit(&results_envelope);
12248 let answer_envelope = provider_answer(
12249 "in-answer",
12250 1_700_000_004_000,
12251 &effect_id(results.step_seq),
12252 "the brief cites three sources",
12253 );
12254 let answered = runtime.submit(&answer_envelope);
12255
12256 let produced = json!({
12257 "description":
12258 "One whole agent turn cycle on the canonical wire (spec 7.9 · Task 12): configure → \
12259 atomic agent start → provider result carrying a tool call → tool results → final \
12260 provider answer → agent terminal. Every step after the start is a single \
12261 `ResolveEffect` input, and each one publishes exactly the next effect the loop is \
12262 waiting on — until the last, whose disposition is the terminal itself and which \
12263 publishes nothing.",
12264 "genesis_digest": genesis.record.record_digest().as_str(),
12265 "head_digest": answered.record.record_digest().as_str(),
12266 "links": [
12267 link(&configure_envelope, &genesis),
12268 link(&start_envelope, &started),
12269 link(&acted_envelope, &acted),
12270 link(&results_envelope, &results),
12271 link(&answer_envelope, &answered),
12272 ],
12273 });
12274
12275 let expected = golden("golden_lifecycle_agent_full_turn.json", &produced);
12276 assert_eq!(produced, expected, "the agent turn cycle drifted");
12277 assert_eq!(
12278 expected["links"][2]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12279 json!("execute_tools"),
12280 "a provider result carrying a host tool call publishes exactly one tool batch",
12281 );
12282 assert_eq!(
12283 expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12284 json!("call_provider"),
12285 "the tool results resume the turn with the next provider call",
12286 );
12287 assert_eq!(
12288 expected["links"][4]["step"]["disposition"]["terminal"]["kind"],
12289 json!("agent"),
12290 );
12291 assert_eq!(
12292 expected["links"][4]["step"]["disposition"]["effects"],
12293 Value::Null,
12294 "§7.12 · effects or a terminal, never both",
12295 );
12296 }
12297
12298 #[test]
12299 fn golden_lifecycle_workflow_root() {
12300 let mut runtime = Runtime::new();
12301 let configure_envelope = configure();
12302 let start_envelope = workflow_start("in-start", 1_700_000_001_000, two_node_spec());
12303 let genesis = runtime.submit(&configure_envelope);
12304 let started = runtime.submit(&start_envelope);
12305
12306 let ack_envelope = spawned(
12307 "in-ack-1",
12308 1_700_000_002_000,
12309 &effect_id(started.step_seq),
12310 &["wf-node0"],
12311 );
12312 let ack = runtime.submit(&ack_envelope);
12313 let done_envelope = child_done("in-done-1", 1_700_000_003_000, "wf-node0", "collected");
12314 let advanced = runtime.submit(&done_envelope);
12315 let ack2_envelope = spawned(
12316 "in-ack-2",
12317 1_700_000_004_000,
12318 &effect_id(advanced.step_seq),
12319 &["wf-node1"],
12320 );
12321 let ack2 = runtime.submit(&ack2_envelope);
12322 let done2_envelope = child_done("in-done-2", 1_700_000_005_000, "wf-node1", "written");
12323 let finished = runtime.submit(&done2_envelope);
12324
12325 let produced = json!({
12326 "description":
12327 "Configure → atomic workflow root start → spawn ack → child completions → workflow \
12328 terminal (spec 10.1). No LoadWorkflow, no placeholder agent run, and no host \
12329 CompleteRun: the last committed step's disposition is the terminal itself.",
12330 "genesis_digest": genesis.record.record_digest().as_str(),
12331 "head_digest": finished.record.record_digest().as_str(),
12332 "links": [
12333 link(&configure_envelope, &genesis),
12334 link(&start_envelope, &started),
12335 link(&ack_envelope, &ack),
12336 link(&done_envelope, &advanced),
12337 link(&ack2_envelope, &ack2),
12338 link(&done2_envelope, &finished),
12339 ],
12340 });
12341
12342 let expected = golden("golden_lifecycle_workflow_root.json", &produced);
12343 assert_eq!(produced, expected, "the workflow root lifecycle drifted");
12344 assert_eq!(expected["links"][1]["step"]["root_kind"], json!("workflow"));
12345 assert_eq!(
12346 expected["links"][1]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12347 json!("spawn_tasks"),
12348 );
12349 assert_eq!(
12350 expected["links"][5]["step"]["disposition"]["terminal"]["kind"],
12351 json!("workflow"),
12352 );
12353 }
12354
12355 #[test]
12356 fn golden_lifecycle_cancel_arc() {
12357 let mut runtime = Runtime::new();
12358 let configure_envelope = signal_config(8, None);
12359 let start_envelope = workflow_start("in-start", 1_700_000_001_000, two_node_spec());
12360 let genesis = runtime.submit(&configure_envelope);
12361 let started = runtime.submit(&start_envelope);
12362
12363 let ack_envelope = spawned(
12364 "in-ack",
12365 1_700_000_002_000,
12366 &effect_id(started.step_seq),
12367 &["wf-node0"],
12368 );
12369 let ack = runtime.submit(&ack_envelope);
12370
12371 let signal_envelope = signal_delivery(
12372 "in-sig",
12373 1_700_000_003_000,
12374 "delivery-a",
12375 1,
12376 logical_signal("sig-abort", SignalUrgency::Critical),
12377 );
12378 let interrupted = runtime.submit(&signal_envelope);
12379
12380 let preempted_envelope = resolved(
12381 "in-preempted",
12382 1_700_000_004_000,
12383 &effect_id(interrupted.step_seq),
12384 EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
12385 attempts: vec![super::super::effect::TaskPreemptOutcome {
12386 task_id: TaskId::new("wf-node0").unwrap(),
12387 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
12388 outcome: super::super::effect::TaskPreemptStatus::Preempted(
12389 super::super::effect::TaskPreempted {},
12390 ),
12391 }],
12392 }),
12393 );
12394 let preempted = runtime.submit(&preempted_envelope);
12395
12396 let cancel_envelope = cancel("in-cancel", 1_700_000_005_000);
12397 let cancelled = runtime.submit(&cancel_envelope);
12398
12399 let produced = json!({
12400 "description":
12401 "The cancellation arc on the canonical wire (spec 7.5 / 7.7 / 11.1 · Task 12b): \
12402 configure → workflow root start → spawn ack → a critical signal that preempts the \
12403 running child → the host's preempt resolution → `HostControl::Cancel`, whose step \
12404 is the cancelled terminal itself. Two things this chain fixes in place: the only \
12405 host action a signal can cause is the preemption (its queueing/dropping/expiry are \
12406 audit facts and publish nothing), and cancellation settles every downstream wait \
12407 inside the same transition that commits the root terminal — §7.12 admits effects or \
12408 a terminal and never both, so there is no second round trip in which the operation \
12409 is neither running nor cancelled.",
12410 "genesis_digest": genesis.record.record_digest().as_str(),
12411 "head_digest": cancelled.record.record_digest().as_str(),
12412 "links": [
12413 link(&configure_envelope, &genesis),
12414 link(&start_envelope, &started),
12415 link(&ack_envelope, &ack),
12416 link(&signal_envelope, &interrupted),
12417 link(&preempted_envelope, &preempted),
12418 link(&cancel_envelope, &cancelled),
12419 ],
12420 });
12421
12422 let expected = golden("golden_lifecycle_cancel_arc.json", &produced);
12423 assert_eq!(produced, expected, "the cancellation arc drifted");
12424 assert_eq!(
12425 expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12426 json!("preempt_tasks"),
12427 "an urgent signal's one host action is stopping the running child",
12428 );
12429 assert_eq!(
12430 expected["links"][5]["step"]["disposition"]["terminal"]["kind"],
12431 json!("cancelled"),
12432 );
12433 assert_eq!(
12434 expected["links"][5]["step"]["disposition"]["effects"],
12435 Value::Null,
12436 "§11.1 · the cancelling step leaves nothing waiting on the host",
12437 );
12438 assert_eq!(
12439 runtime.pending_effect_kinds(),
12440 Vec::<EffectKindTag>::new(),
12441 "and the transaction holds no pending effect after it",
12442 );
12443 }
12444
12445 #[test]
12446 fn golden_lifecycle_external_payload() {
12447 let mut runtime = Runtime::new();
12448 let configure_envelope = payload_config();
12449 let start_envelope = agent_start("in-start", 1_700_000_001_000);
12450 let genesis = runtime.submit(&configure_envelope);
12451 let started = runtime.submit(&start_envelope);
12452
12453 let acted_envelope = provider_result(
12454 "in-acted",
12455 1_700_000_002_000,
12456 &effect_id(started.step_seq),
12457 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12458 );
12459 let acted = runtime.submit(&acted_envelope);
12460 let results_envelope = payloads_resolved(
12461 "in-results",
12462 1_700_000_003_000,
12463 &effect_id(acted.step_seq),
12464 vec![external_payload(
12465 "call-1",
12466 body_digest(),
12467 BODY.len() as u64,
12468 "the full report body, far la…",
12469 )],
12470 );
12471 let results = runtime.submit(&results_envelope);
12472 let read_envelope = provider_result(
12473 "in-read",
12474 1_700_000_004_000,
12475 &effect_id(results.step_seq),
12476 vec![tool_call(
12477 "call-2",
12478 READ_RESULT_TOOL_NAME,
12479 json!({"call_id": "call-1"}),
12480 )],
12481 );
12482 let read = runtime.submit(&read_envelope);
12483 let loaded_envelope = resolved(
12484 "in-loaded",
12485 1_700_000_005_000,
12486 &effect_id(read.step_seq),
12487 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
12488 handle_id: HandleId::new("call-1").unwrap(),
12489 payload: InlinePayload {
12490 content: BODY.to_string(),
12491 digest: body_digest(),
12492 original_size: WireU64::new(BODY.len() as u64),
12493 },
12494 }),
12495 );
12496 let loaded = runtime.submit(&loaded_envelope);
12497
12498 let produced = json!({
12499 "description":
12500 "The §7.10 external-payload arc (Task 13): configure → agent start → a provider \
12501 turn calling one host tool → an **external** tool result → the model's \
12502 `read_result` page-in → the loaded body. Read the records: the persisted body \
12503 appears in exactly one place on this whole chain — the `payload_loaded` outcome \
12504 the host sent when the kernel asked for it. It is in no effect the kernel \
12505 published and in no accepted input the kernel did not ask for, which is what \
12506 §25.10 means by \"large bodies do not enter the journal\". What the kernel holds \
12507 instead is the reference: an opaque `payload_ref` it never interprets, a digest \
12508 it checks the restored body against, and a bounded preview that is the only part \
12509 to occupy context.",
12510 "genesis_digest": genesis.record.record_digest().as_str(),
12511 "head_digest": loaded.record.record_digest().as_str(),
12512 "links": [
12513 link(&configure_envelope, &genesis),
12514 link(&start_envelope, &started),
12515 link(&acted_envelope, &acted),
12516 link(&results_envelope, &results),
12517 link(&read_envelope, &read),
12518 link(&loaded_envelope, &loaded),
12519 ],
12520 });
12521
12522 let expected = golden("golden_lifecycle_external_payload.json", &produced);
12523 assert_eq!(produced, expected, "the external-payload arc drifted");
12524 assert_eq!(
12525 expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12526 json!("call_provider"),
12527 "an external result resumes the turn like any other — the body never comes back out",
12528 );
12529 assert_eq!(
12530 expected["links"][4]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12531 json!("load_payload"),
12532 "§7.10 rule 4 · `read_result` reduces to exactly one effect",
12533 );
12534 assert_eq!(
12535 expected["links"][4]["step"]["disposition"]["effects"][0]["effect"]["payload_ref"],
12536 json!("payload:01J8Y2QK7C4N0V"),
12537 "the effect hands back the host's own opaque locator, unread and unjoined",
12538 );
12539
12540 for (index, link) in expected["links"].as_array().unwrap().iter().enumerate() {
12550 assert_eq!(
12551 link["envelope"]
12552 .to_string()
12553 .contains("clears the inline threshold"),
12554 index == 5,
12555 "link {index} disagrees about where a large body may enter",
12556 );
12557 for effect in link["step"]["disposition"]["effects"]
12560 .as_array()
12561 .unwrap_or(&Vec::new())
12562 {
12563 assert!(
12564 !effect.to_string().contains("clears the inline threshold")
12565 || effect["effect"]["kind"] == json!("call_provider"),
12566 "link {index} hands the body back out in a {} effect",
12567 effect["effect"]["kind"],
12568 );
12569 }
12570 }
12571 for record in &runtime.journal {
12572 let input = serde_json::to_string(&record.normalized_input().unwrap()).unwrap();
12573 assert_eq!(
12574 input.contains("clears the inline threshold"),
12575 record.step_seq().get() == 5,
12576 "only the page-in the model asked for may make a body durable, got step {}",
12577 record.step_seq()
12578 );
12579 }
12580 }
12581
12582 fn agent_mid_turn() -> Runtime {
12589 let mut runtime = Runtime::new();
12590 runtime.submit(&syscall_config());
12591 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
12592 let acted = runtime.submit(&provider_result(
12593 "in-acted",
12594 1_700_000_002_000,
12595 &effect_id(started.step_seq),
12596 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12597 ));
12598 runtime.submit(&tools_resolved(
12599 "in-results",
12600 1_700_000_003_000,
12601 &effect_id(acted.step_seq),
12602 &[("call-1", "three sources found", false)],
12603 ));
12604 runtime
12605 }
12606
12607 #[test]
12608 fn golden_checkpoint_agent_turn() {
12609 let runtime = agent_mid_turn();
12610 let candidate = runtime.checkpoint();
12611 let checkpoint = candidate.decode().expect("the candidate blob verifies");
12612
12613 let produced = json!({
12614 "description":
12615 "A full-state logical checkpoint taken mid-turn (spec 12.1, 12.3). base == through \
12616 == the durable head, so the bounded tail is empty and a restore replays nothing \
12617 before the post-checkpoint records. The logical state is partitioned four ways and \
12618 the header repeats none of it: the pending `call_provider` effect, the replay \
12619 ledger and the terminal slot live in `transition`, the task table in `scheduler`, \
12620 the handle table in `context_vm`, the live policy and the provider-tool causation \
12621 in `syscall`.",
12622 "through_step_seq": candidate.through_step_seq.to_string(),
12623 "covered_head": candidate.covered_head.as_str(),
12624 "state_digest": candidate.state_digest.as_str(),
12625 "ack_token": candidate.ack_token.as_str(),
12626 "checkpoint": serde_json::to_value(&checkpoint).unwrap(),
12627 });
12628
12629 let expected = golden("golden_checkpoint_agent_turn.json", &produced);
12630 assert_eq!(produced, expected, "the logical checkpoint drifted");
12631 assert_eq!(expected["checkpoint"]["checkpoint_version"], json!(1));
12632 assert_eq!(
12633 expected["checkpoint"]["abi_version"],
12634 json!(super::super::KERNEL_ABI_VERSION),
12635 );
12636 assert_eq!(
12637 expected["checkpoint"]["base_step_seq"], expected["checkpoint"]["through_step_seq"],
12638 "a full-state candidate carries no tail",
12639 );
12640 assert_eq!(expected["checkpoint"]["tail_inputs"], json!([]));
12641 assert_eq!(
12642 expected["checkpoint"]["logical_state"]["transition"]["pending_effects"][0]["effect"]["kind"],
12643 json!("call_provider"),
12644 "the effect the operation is waiting on is inside the checkpoint, not beside it",
12645 );
12646 }
12647
12648 #[test]
12652 fn golden_checkpoint_bounded_tail() {
12653 let mut runtime = Runtime::new();
12654 runtime.submit(&syscall_config());
12655 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
12656
12657 let base = runtime
12659 .checkpoint()
12660 .decode()
12661 .expect("the base candidate verifies");
12662
12663 let acted = runtime.submit(&provider_result(
12664 "in-acted",
12665 1_700_000_002_000,
12666 &effect_id(started.step_seq),
12667 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12668 ));
12669 let results = runtime.submit(&tools_resolved(
12670 "in-results",
12671 1_700_000_003_000,
12672 &effect_id(acted.step_seq),
12673 &[("call-1", "three sources found", false)],
12674 ));
12675 runtime.submit(&provider_answer(
12676 "in-answer",
12677 1_700_000_004_000,
12678 &effect_id(results.step_seq),
12679 "the brief cites three sources",
12680 ));
12681
12682 let tail: Vec<CanonicalInput> = runtime.journal[2..]
12683 .iter()
12684 .map(|record| CanonicalInput::from_record(record).expect("a record projects"))
12685 .collect();
12686 assert_eq!(
12690 runtime
12691 .tx
12692 .tail_inputs()
12693 .into_iter()
12694 .filter(|entry| entry.step_seq.get() > base.through_step_seq().get())
12695 .collect::<Vec<_>>(),
12696 tail,
12697 "the transaction's own tail and the journal agree on (base, through]",
12698 );
12699 let rebased = runtime
12700 .tx
12701 .checkpoint_rebase(
12702 &CheckpointBoundary {
12703 through_step_seq: base.through_step_seq(),
12704 covered_head: base.covered_transaction_head_digest().clone(),
12705 },
12706 base.logical_state().clone(),
12707 )
12708 .expect("an older state plus its exact tail is a checkpoint")
12709 .decode()
12710 .expect("the rebase blob verifies");
12711
12712 let produced = json!({
12713 "description":
12714 "The incremental form of a logical checkpoint (spec 12.1, 12.2): `logical_state` is \
12715 the state after `base_step_seq`, and `tail_inputs` covers (base, through] exactly \
12716 — no hole, no duplicate, nothing outside the range. A restore replays this tail on \
12717 top of the state, then continues with the journal records after `through_step_seq`. \
12718 The state digest is byte-identical to the base checkpoint's, because the state is \
12719 the same state; only the tail and the header moved.",
12720 "base_state_digest": base.state_digest().as_str(),
12721 "checkpoint": serde_json::to_value(&rebased).unwrap(),
12722 });
12723
12724 let expected = golden("golden_checkpoint_bounded_tail.json", &produced);
12725 assert_eq!(produced, expected, "the bounded-tail checkpoint drifted");
12726 assert_eq!(
12727 expected["checkpoint"]["state_digest"], expected["base_state_digest"],
12728 "a rebase carries the base state forward untouched",
12729 );
12730 assert_eq!(
12731 expected["checkpoint"]["base_step_seq"],
12732 json!("1"),
12733 "the base is the root start",
12734 );
12735 assert_eq!(expected["checkpoint"]["through_step_seq"], json!("4"));
12736 let tail = expected["checkpoint"]["tail_inputs"].as_array().unwrap();
12737 assert_eq!(
12738 tail.iter()
12739 .map(|entry| entry["step_seq"].as_str().unwrap())
12740 .collect::<Vec<_>>(),
12741 vec!["2", "3", "4"],
12742 "(1, 4] is exactly steps 2, 3 and 4",
12743 );
12744 }
12745
12746 #[test]
12749 fn a_candidate_neither_blocks_nor_is_invalidated_by_later_appends() {
12750 let mut runtime = agent_mid_turn();
12751 let candidate = runtime.checkpoint();
12752 let before = runtime.tx.head().expect("a head");
12753 assert_eq!(candidate.through_step_seq, before.step_seq);
12754
12755 let results_effect = effect_id(before.step_seq);
12756 runtime.submit(&provider_answer(
12757 "in-answer",
12758 1_700_000_004_000,
12759 &results_effect,
12760 "the brief cites three sources",
12761 ));
12762
12763 let after = runtime.tx.head().expect("a head");
12764 assert_ne!(after.step_seq, before.step_seq, "the journal moved on");
12765 assert_eq!(
12766 candidate.through_step_seq, before.step_seq,
12767 "the candidate still covers the prefix it was taken over",
12768 );
12769 candidate
12770 .decode()
12771 .expect("and it still verifies after the journal moved")
12772 .verify_belongs_to(&operation(), runtime.journal[0].record_digest())
12773 .expect("it is still this operation's checkpoint");
12774 }
12775
12776 fn drive(runtime: &mut Runtime, envelopes: &[WireEnvelope]) {
12785 for envelope in envelopes {
12786 runtime.submit(envelope);
12787 }
12788 }
12789
12790 fn turn_envelopes() -> Vec<WireEnvelope> {
12796 vec![
12797 syscall_config(),
12798 agent_start("in-start", 1_700_000_001_000),
12799 provider_result(
12800 "in-acted",
12801 1_700_000_002_000,
12802 &effect_id(WireU64::new(1)),
12803 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12804 ),
12805 tools_resolved(
12806 "in-results",
12807 1_700_000_003_000,
12808 &effect_id(WireU64::new(2)),
12809 &[("call-1", "three sources found", false)],
12810 ),
12811 provider_result(
12812 "in-acted-2",
12813 1_700_000_004_000,
12814 &effect_id(WireU64::new(3)),
12815 vec![tool_call("call-2", "search", json!({"q": "more"}))],
12816 ),
12817 tools_resolved(
12818 "in-results-2",
12819 1_700_000_005_000,
12820 &effect_id(WireU64::new(4)),
12821 &[("call-2", "two more sources", false)],
12822 ),
12823 provider_answer(
12824 "in-answer",
12825 1_700_000_006_000,
12826 &effect_id(WireU64::new(5)),
12827 "the brief cites five sources",
12828 ),
12829 ]
12830 }
12831
12832 fn digests(records: &[KernelRecord]) -> Vec<String> {
12833 records
12834 .iter()
12835 .map(|record| record.record_digest().to_string())
12836 .collect()
12837 }
12838
12839 fn surface(runtime: &Runtime) -> Value {
12846 json!({
12847 "head": runtime.tx.head().map(|head| json!({
12848 "digest": head.digest.as_str(),
12849 "step_seq": head.step_seq.to_string(),
12850 })),
12851 "lifecycle": format!("{:?}", runtime.tx.lifecycle()),
12852 "pending_effects": runtime
12853 .tx
12854 .pending_effects()
12855 .map(|effect| serde_json::to_value(effect).unwrap())
12856 .collect::<Vec<_>>(),
12857 "terminal": runtime.tx.terminal().map(|t| serde_json::to_value(t).unwrap()),
12858 "logical_state": serde_json::to_value(
12859 runtime
12860 .tx
12861 .transition_state_for_restore(
12862 runtime.driver.root_kind(),
12863 runtime.driver.focus().cloned(),
12864 )
12865 .expect("a configured runtime has a transition state"),
12866 )
12867 .unwrap(),
12868 "context_vm": serde_json::to_value(
12869 runtime.driver.project_logical_state().context_vm
12870 ).unwrap(),
12871 "scheduler": serde_json::to_value(
12872 runtime.driver.project_logical_state().scheduler
12873 ).unwrap(),
12874 })
12875 }
12876
12877 #[test]
12881 fn an_active_workflow_and_its_child_restore_to_the_same_completion() {
12882 let (mut uninterrupted, _) = workflow_with_live_child();
12883 let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
12884 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
12885
12886 let first_done = child_done(
12887 "in-done-1",
12888 1_700_000_003_000,
12889 "wf-node0",
12890 "sources collected",
12891 );
12892 let uninterrupted_next = uninterrupted.submit(&first_done);
12893 let restored_next = restored.submit(&first_done);
12894 assert_eq!(
12895 restored_next, uninterrupted_next,
12896 "the restored DAG schedules the same second node",
12897 );
12898
12899 let uninterrupted_spawn = effect_id(uninterrupted_next.step_seq);
12900 let restored_spawn = effect_id(restored_next.step_seq);
12901 uninterrupted.submit(&spawned(
12902 "in-ack-2",
12903 1_700_000_004_000,
12904 &uninterrupted_spawn,
12905 &["wf-node1"],
12906 ));
12907 restored.submit(&spawned(
12908 "in-ack-2",
12909 1_700_000_004_000,
12910 &restored_spawn,
12911 &["wf-node1"],
12912 ));
12913 let second_done = child_done("in-done-2", 1_700_000_005_000, "wf-node1", "brief written");
12914 uninterrupted.submit(&second_done);
12915 restored.submit(&second_done);
12916
12917 assert_eq!(
12918 surface(&restored),
12919 surface(&uninterrupted),
12920 "workflow state and child permission identity are reversible",
12921 );
12922 }
12923
12924 #[test]
12928 fn a_queued_signal_and_its_dedupe_key_restore_to_the_same_follow_up() {
12929 let mut uninterrupted = Runtime::new();
12930 uninterrupted.submit(&signal_config(8, None));
12931 let started = uninterrupted.submit(&agent_start("in-start", 1_700_000_001_000));
12932 uninterrupted.submit(&signal_delivery(
12933 "in-sig-1",
12934 1_700_000_002_000,
12935 "delivery-a",
12936 1,
12937 LogicalSignal {
12938 payload: super::super::scalar::BoundedJson::new(json!({
12939 "job": "nightly-index"
12940 }))
12941 .unwrap(),
12942 dedupe_key: Some("nightly-index".to_string()),
12943 ..logical_signal("sig-nightly", SignalUrgency::Normal)
12944 },
12945 ));
12946
12947 let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
12948 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
12949
12950 let duplicate = signal_delivery(
12951 "in-sig-2",
12952 1_700_000_003_000,
12953 "delivery-b",
12954 2,
12955 LogicalSignal {
12956 payload: super::super::scalar::BoundedJson::new(json!({
12957 "job": "nightly-index"
12958 }))
12959 .unwrap(),
12960 dedupe_key: Some("nightly-index".to_string()),
12961 ..logical_signal("sig-nightly", SignalUrgency::Normal)
12962 },
12963 );
12964 assert_eq!(
12965 restored.submit(&duplicate),
12966 uninterrupted.submit(&duplicate),
12967 "the restored router remembers the business dedupe key",
12968 );
12969
12970 let provider = effect_id(started.step_seq);
12971 let answer = provider_answer(
12972 "in-answer",
12973 1_700_000_004_000,
12974 &provider,
12975 "the first request completed",
12976 );
12977 assert_eq!(
12978 restored.submit(&answer),
12979 uninterrupted.submit(&answer),
12980 "the queued payload produces the same follow-up provider request",
12981 );
12982 assert_eq!(surface(&restored), surface(&uninterrupted));
12983 }
12984
12985 #[test]
12988 fn a_subagent_process_and_join_result_restore_without_permission_drift() {
12989 let mut spec = two_node_spec();
12990 spec.nodes[0].run_spec = Some(LogicalAgentSpec {
12991 role: Some(WireRole::Verify),
12992 isolation: Some(WireIsolation::ReadOnly),
12993 ..LogicalAgentSpec::new("verify the collected sources")
12994 });
12995
12996 let mut uninterrupted = Runtime::new();
12997 uninterrupted.submit(&syscall_config());
12998 let started = uninterrupted.submit(&workflow_start("in-start", 1_700_000_001_000, spec));
12999 uninterrupted.submit(&spawned(
13000 "in-ack-1",
13001 1_700_000_002_000,
13002 &effect_id(started.step_seq),
13003 &["wf-node0"],
13004 ));
13005 let completed = uninterrupted.submit(&child_done(
13006 "in-done-1",
13007 1_700_000_003_000,
13008 "wf-node0",
13009 "sources verified",
13010 ));
13011
13012 let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13013 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13014 let original_task = uninterrupted
13015 .driver
13016 .project_logical_state()
13017 .scheduler
13018 .tasks
13019 .into_iter()
13020 .find(|task| task.task_id.as_str() == "wf-node0")
13021 .expect("the completed child remains projected");
13022 let restored_task = restored
13023 .driver
13024 .project_logical_state()
13025 .scheduler
13026 .tasks
13027 .into_iter()
13028 .find(|task| task.task_id.as_str() == "wf-node0")
13029 .expect("the restored child remains projected");
13030 assert_eq!(restored_task, original_task);
13031 let process = restored_task.process.expect("it is still a child process");
13032 assert_eq!(process.role, "verify");
13033 assert_eq!(process.isolation, "read_only");
13034 assert_eq!(process.context_inheritance, "none");
13035 assert!(
13036 process.join_result.is_some(),
13037 "the join result is source state"
13038 );
13039
13040 let next_spawn = effect_id(completed.step_seq);
13041 let ack = spawned("in-ack-2", 1_700_000_004_000, &next_spawn, &["wf-node1"]);
13042 assert_eq!(
13043 restored.submit(&ack),
13044 uninterrupted.submit(&ack),
13045 "the restored process table authorizes the same next transition",
13046 );
13047 assert_eq!(surface(&restored), surface(&uninterrupted));
13048 }
13049
13050 #[test]
13051 fn a_pending_subagent_preemption_restores_from_the_transition_effect() {
13052 let (mut uninterrupted, _) = workflow_with_live_child();
13053 let requested = uninterrupted.submit(&signal_delivery(
13054 "in-sig-critical",
13055 1_700_000_003_000,
13056 "delivery-critical",
13057 1,
13058 logical_signal("sig-critical", SignalUrgency::Critical),
13059 ));
13060 let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13061 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13062
13063 let resolved = resolved(
13064 "in-preempted",
13065 1_700_000_004_000,
13066 &effect_id(requested.step_seq),
13067 EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
13068 attempts: vec![super::super::effect::TaskPreemptOutcome {
13069 task_id: TaskId::new("wf-node0").unwrap(),
13070 attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
13071 outcome: super::super::effect::TaskPreemptStatus::Preempted(
13072 super::super::effect::TaskPreempted {},
13073 ),
13074 }],
13075 }),
13076 );
13077 assert_eq!(
13078 restored.submit(&resolved),
13079 uninterrupted.submit(&resolved),
13080 "the transition-owned preempt intent remains resolvable after restore",
13081 );
13082 assert_eq!(surface(&restored), surface(&uninterrupted));
13083 }
13084
13085 #[test]
13088 fn a_paged_out_result_restores_to_the_same_rendered_provider_context() {
13089 let mut uninterrupted = Runtime::new();
13090 uninterrupted.submit(&syscall_config());
13091 let started = uninterrupted.submit(&agent_start("in-start", 1_700_000_001_000));
13092 let archived_body = "ARCHIVED TOOL OUTPUT ".repeat(300);
13093 {
13094 let engine = uninterrupted
13095 .driver
13096 .engine
13097 .as_mut()
13098 .expect("the configured agent has an engine");
13099 let mut assistant = Message::assistant("I checked the archive.");
13100 assistant.tool_calls = vec![crate::types::message::ToolCall {
13101 id: "call-archived".into(),
13102 name: "search".into(),
13103 arguments: json!({"q": "archived evidence"}),
13104 }];
13105 engine.ctx.push_history(assistant, 8);
13106 engine.ctx.push_history(
13107 Message::tool(vec![ContentPart::ToolResult {
13108 call_id: "call-archived".into(),
13109 output: archived_body,
13110 is_error: false,
13111 }]),
13112 1_200,
13113 );
13114 let handle_id = engine
13115 .ctx
13116 .handles
13117 .all()
13118 .iter()
13119 .find(|handle| handle.source.as_deref() == Some("call-archived"))
13120 .expect("the tool result is addressable")
13121 .id;
13122 engine
13123 .ctx
13124 .handles
13125 .get_mut(handle_id)
13126 .expect("the handle remains live")
13127 .residency = Residency::PagedOut {
13128 payload_ref: "payload:checkpoint-archive".to_string(),
13129 digest: format!("sha256:{}", "1".repeat(64)),
13130 };
13131 }
13132 let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13133 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13134 assert_eq!(
13135 serde_json::to_value(
13136 &restored
13137 .driver
13138 .engine()
13139 .expect("restored engine")
13140 .ctx
13141 .render()
13142 .turns
13143 )
13144 .unwrap(),
13145 serde_json::to_value(
13146 &uninterrupted
13147 .driver
13148 .engine()
13149 .expect("uninterrupted engine")
13150 .ctx
13151 .render()
13152 .turns
13153 )
13154 .unwrap(),
13155 "the restored PagedOut preview itself matches before either run advances",
13156 );
13157
13158 let acted = provider_result(
13159 "in-acted",
13160 1_700_000_002_000,
13161 &effect_id(started.step_seq),
13162 vec![tool_call(
13163 "call-after-page-out",
13164 "search",
13165 json!({
13166 "q": "fresh evidence"
13167 }),
13168 )],
13169 );
13170 let uninterrupted_tools = uninterrupted.submit(&acted);
13171 let restored_tools = restored.submit(&acted);
13172 assert_eq!(restored_tools, uninterrupted_tools);
13173
13174 let results = tools_resolved(
13175 "in-results",
13176 1_700_000_003_000,
13177 &effect_id(uninterrupted_tools.step_seq),
13178 &[("call-after-page-out", "fresh evidence found", false)],
13179 );
13180 let uninterrupted_provider = uninterrupted.submit(&results);
13181 let restored_provider = restored.submit(&results);
13182 assert_eq!(
13183 restored_provider, uninterrupted_provider,
13184 "the post-restore renderer emits the same provider context with a PagedOut preview",
13185 );
13186 assert_eq!(surface(&restored), surface(&uninterrupted));
13187 }
13188
13189 #[test]
13198 fn an_uninterrupted_run_and_a_full_state_restore_are_byte_identical() {
13199 let envelopes = turn_envelopes();
13200
13201 let mut uninterrupted = Runtime::new();
13202 drive(&mut uninterrupted, &envelopes);
13203
13204 let mut interrupted = Runtime::new();
13205 drive(&mut interrupted, &envelopes[..4]);
13206 let candidate = interrupted.checkpoint();
13207 let checkpoint = candidate.decode().expect("the candidate blob verifies");
13208 assert_eq!(
13209 checkpoint.base_step_seq(),
13210 checkpoint.through_step_seq(),
13211 "this half of the differential exercises the full-state form",
13212 );
13213
13214 let mut restored = interrupted.restore(&checkpoint);
13217 assert_eq!(
13218 restored.restore_cost.unwrap().records_before_checkpoint,
13219 0,
13220 "a restore with a checkpoint reads nothing below it",
13221 );
13222 assert_eq!(
13223 surface(&restored),
13224 surface(&interrupted),
13225 "the restored runtime is the runtime that crashed",
13226 );
13227
13228 drive(&mut restored, &envelopes[4..]);
13229 assert_eq!(
13230 digests(&restored.journal),
13231 digests(&uninterrupted.journal[4..]),
13232 "every post-restore record is byte-identical to the uninterrupted one",
13233 );
13234 assert_eq!(
13235 surface(&restored),
13236 surface(&uninterrupted),
13237 "and so is the state they end in",
13238 );
13239 }
13240
13241 #[test]
13248 fn an_uninterrupted_run_and_a_rebased_restore_are_byte_identical() {
13249 let envelopes = turn_envelopes();
13250
13251 let mut uninterrupted = Runtime::new();
13252 drive(&mut uninterrupted, &envelopes);
13253
13254 let mut interrupted = Runtime::new();
13255 drive(&mut interrupted, &envelopes[..2]);
13256 let base = interrupted
13257 .checkpoint()
13258 .decode()
13259 .expect("the base candidate verifies");
13260 drive(&mut interrupted, &envelopes[2..4]);
13261
13262 let rebased = interrupted
13263 .tx
13264 .checkpoint_rebase(
13265 &CheckpointBoundary {
13266 through_step_seq: base.through_step_seq(),
13267 covered_head: base.covered_transaction_head_digest().clone(),
13268 },
13269 base.logical_state().clone(),
13270 )
13271 .expect("a rebase over (1, 3] is assemblable")
13272 .decode()
13273 .expect("the rebase blob verifies");
13274 assert!(
13275 rebased.base_step_seq() < rebased.through_step_seq(),
13276 "this half of the differential exercises the rebase form",
13277 );
13278 assert_eq!(rebased.tail_inputs().len(), 2);
13279
13280 let mut restored = interrupted.restore(&rebased);
13281 let cost = restored.restore_cost.unwrap();
13282 assert_eq!(cost.records_before_checkpoint, 0);
13283 assert_eq!(
13284 cost.tail_inputs_replayed, 2,
13285 "the tail was actually replayed"
13286 );
13287 assert_eq!(
13288 surface(&restored),
13289 surface(&interrupted),
13290 "replaying the bounded tail lands on the state the run was in",
13291 );
13292
13293 let produced = json!({
13296 "description":
13297 "Spec 12.2 / 12.3 rule 11 · the rebase form end to end. `logical_state` is the \
13298 state after `base_step_seq`, `tail_inputs` covers (base, through] exactly, and \
13299 `base_record_digest` is the chain anchor the tail replays from — the record an \
13300 acked checkpoint is allowed to have reclaimed. Restoring the blob replays that \
13301 tail, verifies each replayed record against the digest the checkpoint carries, and \
13302 lands on the head the run was at.",
13303 "base_step_seq": rebased.base_step_seq().to_string(),
13304 "base_record_digest": rebased.base_record_digest().as_str(),
13305 "through_step_seq": rebased.through_step_seq().to_string(),
13306 "covered_head": rebased.covered_transaction_head_digest().as_str(),
13307 "state_digest": rebased.state_digest().as_str(),
13308 "tail_steps": rebased
13309 .tail_inputs()
13310 .iter()
13311 .map(|entry| entry.step_seq.to_string())
13312 .collect::<Vec<_>>(),
13313 "restored_head": restored.tx.head().unwrap().digest.as_str(),
13314 "restore_cost": {
13315 "records_before_checkpoint": cost.records_before_checkpoint,
13316 "tail_inputs_replayed": cost.tail_inputs_replayed,
13317 "records_after_checkpoint": cost.records_after_checkpoint,
13318 },
13319 });
13320 let expected = golden("golden_checkpoint_rebase_restore.json", &produced);
13321 assert_eq!(produced, expected, "the rebase restore drifted");
13322 assert_eq!(expected["base_step_seq"], json!("1"));
13323 assert_eq!(expected["through_step_seq"], json!("3"));
13324 assert_eq!(
13325 expected["restore_cost"]["records_before_checkpoint"],
13326 json!(0),
13327 "the whole point: nothing below the base is read",
13328 );
13329
13330 drive(&mut restored, &envelopes[4..]);
13331 assert_eq!(
13332 digests(&restored.journal),
13333 digests(&uninterrupted.journal[4..]),
13334 );
13335 assert_eq!(surface(&restored), surface(&uninterrupted));
13336 }
13337
13338 #[test]
13344 fn an_external_body_is_checkpointed_by_reference_and_restored_as_one() {
13345 let (mut runtime, effect) = agent_awaiting_tool_results();
13346 let body_digest =
13347 super::super::record::canonical_digest(b"a body far over the inline threshold");
13348 runtime.submit(&payloads_resolved(
13349 "in-results",
13350 1_700_000_003_000,
13351 &effect,
13352 vec![external_payload(
13353 "call-1",
13354 body_digest.clone(),
13355 64 * 1024,
13356 "the first 2 KiB of it",
13357 )],
13358 ));
13359
13360 let context_vm = runtime.driver.project_logical_state().context_vm;
13361 let referenced: Vec<&StoredMessageState> = context_vm
13362 .messages
13363 .iter()
13364 .filter(|message| matches!(message.body, StoredMessageBody::Reference(_)))
13365 .collect();
13366 assert_eq!(referenced.len(), 1, "exactly the external result");
13367 let StoredMessageBody::Reference(reference) = &referenced[0].body else {
13368 unreachable!()
13369 };
13370 assert_eq!(reference.digest, body_digest.as_str());
13371 assert_eq!(reference.tool_call_id.as_deref(), Some("call-1"));
13372 assert_eq!(reference.preview, "the first 2 KiB of it");
13373 assert!(
13374 !serde_json::to_string(&context_vm)
13375 .unwrap()
13376 .contains("a body far over the inline threshold"),
13377 "the body itself is nowhere in the checkpoint",
13378 );
13379
13380 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13381 let restored = Runtime::restore_with(Some(&checkpoint), &[]);
13382 assert_eq!(surface(&restored), surface(&runtime));
13383 assert_eq!(
13384 restored
13385 .driver
13386 .project_logical_state()
13387 .context_vm
13388 .handles
13389 .iter()
13390 .filter_map(|handle| handle.digest.clone())
13391 .collect::<Vec<_>>(),
13392 vec![body_digest.to_string()],
13393 "the handle that addresses the body is restored with its verification digest",
13394 );
13395 }
13396
13397 #[test]
13403 fn a_rebase_and_a_full_state_candidate_agree_on_the_state_digest() {
13404 let envelopes = turn_envelopes();
13405 let mut runtime = Runtime::new();
13406 drive(&mut runtime, &envelopes[..2]);
13407 let base = runtime
13408 .checkpoint()
13409 .decode()
13410 .expect("the base candidate verifies");
13411 drive(&mut runtime, &envelopes[2..4]);
13412
13413 let full_state = runtime.checkpoint();
13414 let rebase = runtime
13415 .tx
13416 .checkpoint_rebase(
13417 &CheckpointBoundary {
13418 through_step_seq: base.through_step_seq(),
13419 covered_head: base.covered_transaction_head_digest().clone(),
13420 },
13421 base.logical_state().clone(),
13422 )
13423 .expect("a rebase is assemblable");
13424
13425 assert_eq!(
13426 full_state.through_step_seq, rebase.through_step_seq,
13427 "both cover the same prefix",
13428 );
13429 assert_eq!(full_state.covered_head, rebase.covered_head);
13430 assert_ne!(
13431 full_state.state_digest, rebase.state_digest,
13432 "they carry *different* logical states — one at the head, one at the base",
13433 );
13434 assert_eq!(
13435 rebase.state_digest,
13436 *base.state_digest(),
13437 "and a rebase carries its base's state forward untouched, byte for byte",
13438 );
13439
13440 let from_full = runtime.restore(&full_state.decode().unwrap());
13443 let from_rebase = runtime.restore(&rebase.decode().unwrap());
13444 assert_eq!(surface(&from_full), surface(&from_rebase));
13445 assert_eq!(surface(&from_full), surface(&runtime));
13446 }
13447
13448 #[test]
13454 fn long_run_restore_cost_is_bounded_by_the_tail_not_the_run() {
13455 fn cost_after(turns: usize) -> RestoreCost {
13456 let mut runtime = Runtime::new();
13457 runtime.submit(&syscall_config_with(|config| {
13458 config.execution_policy = Some(ExecutionPolicy {
13459 max_turns: Some(10_000),
13460 ..ExecutionPolicy::default()
13461 });
13462 }));
13463 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13464 let mut effect = effect_id(started.step_seq);
13465 let mut at = 1_700_000_002_000;
13466 for turn in 0..turns {
13467 let acted = runtime.submit(&provider_result(
13468 &format!("in-acted-{turn}"),
13469 at,
13470 &effect,
13471 vec![tool_call(
13472 &format!("call-{turn}"),
13473 "search",
13474 json!({ "q": turn }),
13475 )],
13476 ));
13477 at += 1_000;
13478 let results = runtime.submit(&tools_resolved(
13479 &format!("in-results-{turn}"),
13480 at,
13481 &effect_id(acted.step_seq),
13482 &[(&format!("call-{turn}"), "a source", false)],
13483 ));
13484 at += 1_000;
13485 effect = effect_id(results.step_seq);
13486 }
13487
13488 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13490 let after = runtime.journal_from(&checkpoint);
13491 assert!(after.is_empty(), "the checkpoint covers the whole journal");
13492 Runtime::restore_with(Some(&checkpoint), &after)
13493 .restore_cost
13494 .expect("a restored runtime reports its cost")
13495 }
13496
13497 let short = cost_after(2);
13498 let medium = cost_after(8);
13499 let long = cost_after(32);
13500
13501 assert_eq!(short.total_transitions(), 0, "nothing is replayed at all");
13502 assert_eq!(
13503 (short, medium),
13504 (medium, long),
13505 "restore cost does not grow with the length of the run",
13506 );
13507
13508 let mut runtime = Runtime::new();
13511 drive(&mut runtime, &turn_envelopes());
13512 let journal = runtime.journal.clone();
13513 let from_genesis = Runtime::restore_with(None, &journal);
13514 assert_eq!(
13515 from_genesis.restore_cost.unwrap().records_before_checkpoint,
13516 journal.len() as u64,
13517 "the no-checkpoint arm is O(run), which is exactly what §12 replaces",
13518 );
13519 assert_eq!(surface(&from_genesis), surface(&runtime));
13520 }
13521
13522 #[test]
13524 fn a_restore_without_a_checkpoint_uses_the_same_transaction_fold() {
13525 let mut runtime = Runtime::new();
13526 drive(&mut runtime, &turn_envelopes()[..4]);
13527 let from_genesis = Runtime::restore_with(None, &runtime.journal);
13528 assert_eq!(surface(&from_genesis), surface(&runtime));
13529 assert_eq!(from_genesis.restore_cost.unwrap().tail_inputs_replayed, 0);
13530 }
13531
13532 #[test]
13538 fn crash_matrix_install_then_crash_before_ack_restores_from_the_installed_checkpoint() {
13539 let envelopes = turn_envelopes();
13540 let mut runtime = Runtime::new();
13541 drive(&mut runtime, &envelopes[..4]);
13542
13543 let candidate = runtime.checkpoint();
13544 let checkpoint = candidate.decode().expect("the host installed this blob");
13545 let restored = runtime.restore(&checkpoint);
13547 assert_eq!(surface(&restored), surface(&runtime));
13548 assert_eq!(restored.restore_cost.unwrap().records_before_checkpoint, 0);
13549
13550 let recovered = restore_operation(
13552 Some(&checkpoint),
13553 &[],
13554 ConfigDefaults::default(),
13555 InMemoryRecordIndex::new(),
13556 )
13557 .expect("the ladder runs");
13558 assert_eq!(
13559 recovered
13560 .pending_effects()
13561 .iter()
13562 .map(|effect| effect.tag())
13563 .collect::<Vec<_>>(),
13564 vec![EffectKindTag::CallProvider],
13565 "§5g-1 · the effect the operation is waiting on is exposed again",
13566 );
13567 assert!(recovered.terminal().is_none(), "the run had not ended");
13568 }
13569
13570 #[test]
13573 fn crash_matrix_appends_after_a_candidate_are_restored_from_the_journal() {
13574 let envelopes = turn_envelopes();
13575 let mut runtime = Runtime::new();
13576 drive(&mut runtime, &envelopes[..4]);
13577
13578 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13579 drive(&mut runtime, &envelopes[4..6]);
13581
13582 let restored = runtime.restore(&checkpoint);
13583 let cost = restored.restore_cost.unwrap();
13584 assert_eq!(
13585 cost.records_after_checkpoint, 2,
13586 "the two records appended after the candidate are replayed from the journal",
13587 );
13588 assert_eq!(cost.records_before_checkpoint, 0);
13589 assert_eq!(surface(&restored), surface(&runtime));
13590 }
13591
13592 #[test]
13595 fn crash_matrix_install_when_the_covered_head_has_moved_on() {
13596 let envelopes = turn_envelopes();
13597 let mut runtime = Runtime::new();
13598 drive(&mut runtime, &envelopes[..4]);
13599 let candidate = runtime.checkpoint();
13600 let covered = candidate.through_step_seq;
13601
13602 drive(&mut runtime, &envelopes[4..]);
13603 assert_ne!(
13604 runtime.tx.head().unwrap().step_seq,
13605 covered,
13606 "the journal has moved past the covered head",
13607 );
13608
13609 let mut acked = Runtime::new();
13612 drive(&mut acked, &envelopes);
13613 let usage = acked
13614 .tx
13615 .note_checkpoint_acked(&candidate.boundary())
13616 .expect("a checkpoint that covers a prefix is ackable after the head moved");
13617 assert_eq!(
13618 usage.records, 3,
13619 "acking reclaims the covered prefix and keeps the rest as tail",
13620 );
13621
13622 let restored = runtime.restore(&candidate.decode().unwrap());
13624 assert_eq!(surface(&restored), surface(&runtime));
13625 }
13626
13627 #[test]
13633 fn crash_matrix_ack_then_prune_then_restore_still_answers_a_redelivery() {
13634 let envelopes = turn_envelopes();
13635 let mut runtime = Runtime::new();
13636 drive(&mut runtime, &envelopes[..4]);
13637
13638 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13639 runtime
13640 .tx
13641 .note_checkpoint_acked(&checkpoint.boundary())
13642 .expect("the boundary names a prefix of this journal");
13643
13644 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13647 assert_eq!(surface(&restored), surface(&runtime));
13648
13649 let redelivered = restored.prepare(&envelopes[3]);
13651 let RecordPreparation::Replayed(replay) = redelivered else {
13652 panic!("a redelivery below the checkpoint base must not be accepted again");
13653 };
13654 assert_eq!(replay.step_seq, WireU64::new(3));
13655 assert_eq!(
13656 replay.record_digest,
13657 *runtime.journal[3].record_digest(),
13658 "§12.3 rule 10 · the answer is the original step and record digest",
13659 );
13660 assert!(
13661 replay.committed_step.is_none() && replay.record.is_none(),
13662 "and it carries no step payload — the guarantee down there is idempotent \
13663 acknowledgement, not step reproduction",
13664 );
13665
13666 drive(&mut restored, &envelopes[4..]);
13668 let mut uninterrupted = Runtime::new();
13669 drive(&mut uninterrupted, &envelopes);
13670 assert_eq!(surface(&restored), surface(&uninterrupted));
13671 }
13672
13673 #[test]
13675 fn a_restore_preserves_effect_terminal_attempt_and_handle_identity() {
13676 let envelopes = turn_envelopes();
13677 let mut runtime = Runtime::new();
13678 drive(&mut runtime, &envelopes[..4]);
13679
13680 let before_effects: Vec<String> = runtime
13681 .tx
13682 .pending_effects()
13683 .map(|effect| effect.effect_id.to_string())
13684 .collect();
13685 let before_handles =
13686 serde_json::to_value(runtime.driver.project_logical_state().context_vm.handles)
13687 .unwrap();
13688 let before_attempts =
13689 serde_json::to_value(runtime.driver.project_logical_state().scheduler.attempts)
13690 .unwrap();
13691
13692 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13693 let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13694
13695 assert!(!before_effects.is_empty(), "the fixture has live identity");
13696 assert_eq!(
13697 restored
13698 .tx
13699 .pending_effects()
13700 .map(|effect| effect.effect_id.to_string())
13701 .collect::<Vec<_>>(),
13702 before_effects,
13703 "§5g-1 · appended-but-unpublished effects are re-exposed under their own ids",
13704 );
13705 assert_eq!(
13706 serde_json::to_value(restored.driver.project_logical_state().context_vm.handles)
13707 .unwrap(),
13708 before_handles,
13709 "handle identity survives",
13710 );
13711 assert_eq!(
13712 serde_json::to_value(restored.driver.project_logical_state().scheduler.attempts)
13713 .unwrap(),
13714 before_attempts,
13715 "task attempt identity survives",
13716 );
13717
13718 drive(&mut restored, &envelopes[4..]);
13720 let terminal = restored.tx.terminal().cloned().expect("the run ended");
13721 let after_terminal =
13722 Runtime::restore_with(Some(&restored.checkpoint().decode().unwrap()), &[]);
13723 assert_eq!(
13724 after_terminal.tx.terminal(),
13725 Some(&terminal),
13726 "the terminal is restored as the same terminal, not re-derived",
13727 );
13728 }
13729
13730 #[test]
13735 fn a_full_tail_refuses_retryably_and_the_same_envelope_succeeds_after_an_ack() {
13736 let mut runtime = Runtime::new();
13737 runtime.submit(&syscall_config_with(|config| {
13738 config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13739 provider_recovery_attempts: None,
13740 output_recovery_attempts: None,
13741 tail_bounds: Some(super::super::command::TailBoundsPolicy {
13742 soft_records: Some(WireU64::new(2)),
13743 hard_records: Some(WireU64::new(3)),
13744 soft_bytes: Some(WireU64::new(64 * 1024)),
13745 hard_bytes: Some(WireU64::new(1024 * 1024)),
13746 }),
13747 });
13748 }));
13749 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13750 let acted = runtime.submit(&provider_result(
13751 "in-acted",
13752 1_700_000_002_000,
13753 &effect_id(started.step_seq),
13754 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
13755 ));
13756
13757 let next = tools_resolved(
13758 "in-results",
13759 1_700_000_003_000,
13760 &effect_id(acted.step_seq),
13761 &[("call-1", "three sources found", false)],
13762 );
13763 let fault = runtime.reject(&next);
13764 assert_eq!(fault.code, KernelFaultCode::CheckpointRequired);
13765 assert!(fault.is_retryable(), "exactly one code says retry");
13766
13767 let produced = json!({
13770 "expect": "checkpoint_required",
13771 "description":
13772 "Spec 12.3 · the next transaction would carry the journal tail past its hard limit, \
13773 so `prepare` refuses with the one retryable fault code and zero mutation. The \
13774 input was never accepted: after a checkpoint candidate is installed and acked, the \
13775 *same* envelope — same input id, same clock, same payload — is submitted again and \
13776 commits. There is no permanent overflow latch: an acked checkpoint takes the tail \
13777 pressure straight back to nominal.",
13778 "tail_bounds": {
13779 "soft_records": "2",
13780 "hard_records": "3",
13781 "soft_bytes": "65536",
13782 "hard_bytes": "1048576",
13783 },
13784 "tail_usage_at_refusal": {
13785 "records": runtime.tx.tail_usage().records.to_string(),
13786 },
13787 "refused_envelope": serde_json::to_value(&next).unwrap(),
13788 "fault": serde_json::to_value(&fault).unwrap(),
13789 "retryable": fault.is_retryable(),
13790 });
13791 let expected = golden(
13792 "reject_transaction_checkpoint_required_tail_full.json",
13793 &produced,
13794 );
13795 assert_eq!(produced, expected, "the CheckpointRequired refusal drifted");
13796
13797 let head_before = runtime.tx.head().expect("a head");
13800 let candidate = runtime.checkpoint();
13801 assert_eq!(candidate.through_step_seq, head_before.step_seq);
13802
13803 runtime
13804 .tx
13805 .note_checkpoint_acked(&candidate.boundary())
13806 .expect("the ack names this journal's head");
13807
13808 let committed = runtime.submit(&next);
13810 assert_eq!(committed.step_seq, WireU64::new(3));
13811 assert_eq!(
13812 runtime.tx.tail_pressure(),
13813 TailPressure::Nominal,
13814 "an acked checkpoint moves the pressure straight back — there is no latch",
13815 );
13816 }
13817
13818 #[test]
13820 fn the_soft_watermark_is_advice_delivered_once_on_the_crossing() {
13821 let mut runtime = Runtime::new();
13822 let genesis = runtime.submit(&syscall_config_with(|config| {
13823 config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13824 provider_recovery_attempts: None,
13825 output_recovery_attempts: None,
13826 tail_bounds: Some(super::super::command::TailBoundsPolicy {
13827 soft_records: Some(WireU64::new(2)),
13828 hard_records: Some(WireU64::new(8)),
13829 soft_bytes: Some(WireU64::new(64 * 1024)),
13830 hard_bytes: Some(WireU64::new(1024 * 1024)),
13831 }),
13832 });
13833 }));
13834 assert!(
13835 genesis.checkpoint_advice.is_none(),
13836 "one record is not a watermark crossing",
13837 );
13838
13839 let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13840 let advice = started
13841 .checkpoint_advice
13842 .expect("the second record takes the tail to the soft watermark");
13843 assert_eq!(advice.through_step_seq, started.step_seq);
13844 assert_eq!(advice.usage.records, 2);
13845 assert_eq!(advice.bounds.soft_records, WireU64::new(2));
13846
13847 let acted = runtime.submit(&provider_result(
13848 "in-acted",
13849 1_700_000_002_000,
13850 &effect_id(started.step_seq),
13851 vec![tool_call("call-1", "search", json!({"q": "sources"}))],
13852 ));
13853 assert!(
13854 acted.checkpoint_advice.is_none(),
13855 "advice is edge-triggered: staying over the watermark is not news",
13856 );
13857 }
13858
13859 #[test]
13866 fn rebuilding_after_a_conflict_costs_the_tail_not_the_run() {
13867 let envelopes = turn_envelopes();
13868 let mut runtime = Runtime::new();
13869 drive(&mut runtime, &envelopes[..6]);
13870 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13871
13872 let preparation = runtime.prepare(&envelopes[6]);
13875 let token = preparation.token().expect("prepared").clone();
13876 let fault = runtime.tx.note_append_conflict(&token, None);
13877 assert_eq!(fault.code, KernelFaultCode::TransactionConflict);
13878 assert!(runtime.tx.is_poisoned());
13879
13880 let rebuilt = Runtime::restore_with(Some(&checkpoint), &[]);
13881 let cost = rebuilt.restore_cost.unwrap();
13882 assert_eq!(
13883 cost.total_transitions(),
13884 0,
13885 "the rebuild reads the checkpoint and nothing else — O(tail), not O(run)",
13886 );
13887 assert!(!rebuilt.tx.is_poisoned());
13888 }
13889
13890 #[test]
13895 fn a_restore_that_does_not_reproduce_the_captured_state_fails_closed() {
13896 let envelopes = turn_envelopes();
13897 let mut runtime = Runtime::new();
13898 drive(&mut runtime, &envelopes[..4]);
13899 let checkpoint = runtime.checkpoint().decode().expect("verifies");
13900
13901 let mut state = checkpoint.logical_state().clone();
13908 state.context_vm.partition_tokens.history += 7;
13909 let forged = KernelCheckpoint::assemble(CheckpointDraft {
13910 operation_id: operation(),
13911 genesis_digest: checkpoint.genesis_digest().clone(),
13912 base_step_seq: checkpoint.base_step_seq(),
13913 base_record_digest: checkpoint.base_record_digest().clone(),
13914 through_step_seq: checkpoint.through_step_seq(),
13915 covered_transaction_head_digest: checkpoint.covered_transaction_head_digest().clone(),
13916 logical_state: state,
13917 tail_inputs: Vec::new(),
13918 })
13919 .expect("a self-consistent checkpoint over a different state");
13920
13921 let error = restore_operation(
13922 Some(&forged),
13923 &[],
13924 ConfigDefaults::default(),
13925 InMemoryRecordIndex::new(),
13926 )
13927 .expect_err("a state that does not come back is a refusal, not a warning");
13928 assert_eq!(error.code, KernelFaultCode::CheckpointCorrupted);
13929 assert!(error.message.contains("restored logical state hashes to"));
13930 }
13931
13932 #[test]
13935 fn the_tail_bound_comes_from_the_genesis_configuration() {
13936 let mut runtime = Runtime::new();
13937 assert_eq!(
13938 runtime.tx.bounds(),
13939 TailBounds::DEFAULT,
13940 "before genesis the transaction runs on the bootstrap baseline",
13941 );
13942
13943 runtime.submit(&syscall_config_with(|config| {
13944 config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13945 provider_recovery_attempts: None,
13946 output_recovery_attempts: None,
13947 tail_bounds: Some(super::super::command::TailBoundsPolicy {
13948 soft_records: Some(WireU64::new(4)),
13949 hard_records: Some(WireU64::new(8)),
13950 soft_bytes: Some(WireU64::new(64 * 1024)),
13951 hard_bytes: Some(WireU64::new(256 * 1024)),
13952 }),
13953 });
13954 }));
13955
13956 assert_eq!(
13957 runtime.tx.bounds(),
13958 TailBounds::new(4, 8, 64 * 1024, 256 * 1024).unwrap(),
13959 "the genesis record's resolved configuration is what bounds the tail",
13960 );
13961 assert_eq!(
13962 runtime
13963 .tx
13964 .config()
13965 .expect("configured")
13966 .recovery_policy
13967 .tail_bounds,
13968 runtime.tx.bounds(),
13969 "and it is frozen in the record, so a rebuild re-derives the same bound",
13970 );
13971 }
13972}