1use std::collections::{BTreeMap, BTreeSet};
47
48use serde::{Deserialize, Serialize};
49
50use super::checkpoint::{
51 AuthoredMemoryQueryState, AuthoredMemoryWriteState, ChildProcessState, ContextVmState,
52 EntropyState, EntropyTurnState, HandleState, InlineMessageBody, KnowledgeSlotState,
53 LocalChannelState, LogicalCompressionEntry, LogicalKernelState, LogicalPlanStep,
54 LogicalStateProjection, LogicalTaskState, LogicalToolCall, MessagePartition, MilestoneState,
55 PartitionTokenState, PendingPayloadLoadState, PendingProviderCallState, QueuedSignalState,
56 ReferencedMessageBody, SchedulerState, SkillLeaseState, StoredMessageBody, StoredMessageState,
57 StructuredMessageBody, SyscallState, TaskAttemptState, TaskControlState,
58 TaskWaitConditionState, TaskWaitSetState, WorkflowGraphState, WorkflowNodeState,
59};
60use super::command::{
61 ApplyCapabilityPatchCommand, ApplyKnowledgeMutationCommand, ApplyPolicyPatchCommand,
62 ApplySkillActivationCommand, CancelCommand, CancellationReason, HostCommand, LivePolicyState,
63 SeedKnowledgeCommand, TaskUpdate as WireTaskUpdate, UpdateDeadlineCommand, UpdateTaskCommand,
64};
65use super::config::ResolvedOperationConfig;
66use super::effect::{
67 ApprovalRequest as WireApprovalRequest, ArchivePageOutEffect, CallProviderEffect,
68 CanonicalMemoryQuery, CanonicalMemoryWrite, EffectKind, EffectKindTag, EffectOutcome,
69 EffectSuccess, EvaluateMilestoneEffect, ExecuteToolsEffect, HostEffectFailure, KernelEffect,
70 LaunchToken, LoadPayloadEffect, PageOutPayload, PayloadRef, PersistMemoryEffect,
71 PreemptTasksEffect, ProviderCompleted, ProviderMessage, ProviderOutcome, QueryMemoryEffect,
72 RenderedContext as WireRenderedContext, RequestApprovalEffect, SpawnTasksEffect,
73 TaskAttemptRef, TaskLaunch, ToolCall as WireToolCall, ToolResultDisposition,
74 ToolResultPayload as WireToolResultPayload, ToolSchema as WireToolSchema,
75 WorkflowBudget as WireWorkflowBudget,
76};
77use super::envelope::{OperationLifecycle, ResolveEffect};
78use super::event::{
79 ChildCompleted, ChildStatus, DeliverSignal, ExternalEvent, LogicalSignal, SignalSourceKind,
80 SignalTarget, SignalUrgency,
81};
82use super::fault::{KernelFault, KernelFaultCode};
83use super::record::NormalizedPayload;
84use super::root::{
85 AgentIsolation as WireIsolation, AgentRole as WireRole, ExecutionFocus, InitialContext,
86 LogicalAgentSpec, LogicalContextInheritance as WireContextInheritance, LogicalTask,
87 MessageRole, RootEntry, RootKind, WorkflowNode as WireNode, WorkflowSpec as WireSpec,
88};
89use super::scalar::{
90 AttemptId, EffectId, MemoryBindingId, NodeId, OperationId, TaskId, WireU64, WorkflowId,
91};
92use super::syscall::{
93 ChildAttemptCausation, MemoryKind as WireMemoryKind, ProviderToolCausation, SyscallCausation,
94 SyscallRequest,
95};
96use super::terminal::{
97 AgentTerminal, CancelledTerminal, EffectsDisposition, FailedTerminal, KernelFailure,
98 KernelFailureCode, KernelTerminal, LoopResult as WireLoopResult, StepDisposition,
99 TerminalDisposition, TerminationReason as WireTermination, UsageReport, WorkflowOutcome,
100 WorkflowStatus, WorkflowTerminal,
101};
102use super::transaction::{PlanContext, TransitionStep};
103
104use crate::context::manager::READ_RESULT_TOOL_NAME;
105use crate::context::task_state::{CompressionEntry, PlanStep, TaskState};
106use crate::mm::handle::{Handle, HandleKind, Residency};
107use crate::orchestration::task_graph::TaskStatus;
108use crate::orchestration::workflow::run::{WorkflowNodeStatus, WorkflowRuntimeNodeState};
109use crate::orchestration::workflow::{
110 WorkflowNode as CoreWorkflowNode, WorkflowSpec as CoreWorkflowSpec,
111};
112use crate::runtime::kernel::{KernelObservation, WorkflowSpawnFailure};
113use crate::scheduler::policy::SchedulerBudget;
114use crate::scheduler::state_machine::{
115 AdjudicatedTurn, AnsweredCall, IdleContinuation, LoopAction, LoopEvent, LoopStateMachine,
116};
117use crate::scheduler::tcb::{
118 ApprovalId, BudgetLedger, ChannelId, DurableWaitSet, LogicalDeadline, ProcInfo, ResourceKey,
119 SignalFilter, SubscriptionId, TaskLifecycle, Tcb, WaitCondition, WaitMode,
120};
121use crate::scheduler::wait_index::WaitKey;
122use crate::signals::queue::QueuedSignalRuntimeState;
123use crate::signals::router::SignalRouterRuntimeState;
124use crate::syscall::{Disposition, Syscall as CoreSyscall};
125use crate::types::agent::{
126 AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec,
127 ContextInheritance, LoopRoundSpec,
128};
129use crate::types::durable_content::{
130 DurableContent, DurableContentBlock, DurableSource, DurableToolResult,
131};
132use crate::types::message::{Content, ContentPart, CoreMessage, Role, ToolErrorKind, ToolResult};
133use crate::types::result::{
134 LoopResult, PaceAction as CorePaceAction, SubAgentResult, TerminationReason,
135};
136use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
137use crate::types::task::{RuntimeTask, TaskLane};
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct PlannedStep {
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub root_kind: Option<RootKind>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub focus: Option<ExecutionFocus>,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub observations: Vec<KernelObservation>,
163 pub disposition: StepDisposition,
164}
165
166impl PartialEq for PlannedStep {
167 fn eq(&self, other: &Self) -> bool {
168 self.root_kind == other.root_kind
169 && self.focus == other.focus
170 && self.disposition == other.disposition
171 && serde_json::to_vec(&self.observations).ok()
172 == serde_json::to_vec(&other.observations).ok()
173 }
174}
175
176impl PlannedStep {
177 fn quiet(root_kind: Option<RootKind>, focus: Option<ExecutionFocus>) -> Self {
178 Self {
179 root_kind,
180 focus,
181 observations: Vec::new(),
182 disposition: StepDisposition::Effects(EffectsDisposition::default()),
183 }
184 }
185}
186
187impl TransitionStep for PlannedStep {
188 fn disposition(&self) -> &StepDisposition {
189 &self.disposition
190 }
191}
192
193pub const ROOT_TASK_ID: &str = "root";
200
201const NO_HOST_SESSION: &str = "";
204
205#[derive(Debug, Clone, PartialEq)]
206struct StagedFocus {
207 step_seq: WireU64,
208 root_kind: Option<RootKind>,
209 focus: Option<ExecutionFocus>,
210}
211
212#[derive(Debug, Clone, PartialEq)]
222struct PendingProviderCall {
223 task_id: TaskId,
224 exposed_tools: BTreeSet<String>,
225}
226
227#[derive(Debug, Clone, PartialEq)]
235struct SyscallRejection {
236 operation: &'static str,
237 subject: Option<String>,
240 reason: String,
241}
242
243impl SyscallRejection {
244 fn new(operation: &'static str, reason: impl Into<String>) -> Self {
245 Self {
246 operation,
247 subject: None,
248 reason: reason.into(),
249 }
250 }
251
252 fn by(mut self, caller: &TaskId) -> Self {
253 self.subject = Some(caller.as_str().to_string());
254 self
255 }
256}
257
258#[derive(Debug, Clone)]
260enum SyscallRefusal {
261 Fault(KernelFault),
264 Rejected(SyscallRejection),
267}
268
269fn handle_kind_label(kind: &HandleKind) -> &'static str {
275 match kind {
276 HandleKind::ToolResult => "tool_result",
277 HandleKind::MemoryPage => "memory_page",
278 HandleKind::KnowledgeEntry => "knowledge_entry",
279 HandleKind::SubAgentJoin => "sub_agent_join",
280 }
281}
282
283fn role_label(role: Role) -> &'static str {
284 match role {
285 Role::System => "system",
286 Role::User => "user",
287 Role::Assistant => "assistant",
288 Role::Tool => "tool",
289 }
290}
291
292fn role_from_label(label: &str) -> Option<Role> {
293 match label {
294 "system" => Some(Role::System),
295 "user" => Some(Role::User),
296 "assistant" => Some(Role::Assistant),
297 "tool" => Some(Role::Tool),
298 _ => None,
299 }
300}
301
302#[allow(clippy::type_complexity)]
308fn message_body_parts(message: &CoreMessage) -> Option<(String, Option<String>, bool)> {
309 match &message.content {
310 Content::Text(text) => Some((text.clone(), None, false)),
311 Content::Parts(parts) => {
312 let mut text = String::new();
313 let mut tool_call_id = None;
314 let mut is_error = false;
315 for part in parts {
316 match part {
317 ContentPart::Text { text: chunk } => text.push_str(chunk),
318 ContentPart::ToolResult {
319 call_id,
320 output,
321 is_error: failed,
322 durable_content,
323 } => {
324 if tool_call_id.is_some() {
325 return None;
328 }
329 tool_call_id = Some(call_id.to_string());
330 if durable_content.is_some() {
331 return None;
334 }
335 text.push_str(output);
336 is_error = *failed;
337 }
338 ContentPart::Image { .. } | ContentPart::Audio { .. } => return None,
339 }
340 }
341 Some((text, tool_call_id, is_error))
342 }
343 }
344}
345
346fn message_content(text: String, tool_call_id: Option<&str>, is_error: bool) -> Content {
351 match tool_call_id {
352 Some(call_id) => Content::Parts(vec![ContentPart::ToolResult {
353 call_id: call_id.into(),
354 output: text,
355 is_error,
356 durable_content: None,
357 }]),
358 None => Content::Text(text),
359 }
360}
361
362fn content_to_durable(content: &Content) -> Result<DurableContent, String> {
363 let blocks = match content {
364 Content::Text(text) => vec![DurableContentBlock::Text { text: text.clone() }],
365 Content::Parts(parts) => parts
366 .iter()
367 .map(content_part_to_durable)
368 .collect::<Result<Vec<_>, _>>()?,
369 };
370 let content = DurableContent { blocks };
371 content.validate().map_err(|error| error.to_string())?;
372 Ok(content)
373}
374
375fn content_part_to_durable(part: &ContentPart) -> Result<DurableContentBlock, String> {
376 match part {
377 ContentPart::Text { text } => Ok(DurableContentBlock::Text { text: text.clone() }),
378 ContentPart::ToolResult { .. } => Err(
379 "a structured message cannot embed a tool result; durable tool results use their separate envelope".into(),
380 ),
381 ContentPart::Image { source, media_type, detail } => {
382 let provider_options = detail
383 .as_ref()
384 .map(|detail| serde_json::json!({ "detail": detail }));
385 Ok(DurableContentBlock::Image {
386 source: source.clone(),
387 media_type: media_type.clone(),
388 provider_options,
389 })
390 }
391 ContentPart::Audio { source, media_type } => Ok(DurableContentBlock::Audio {
392 source: source.clone(),
393 media_type: Some(media_type.clone()),
394 provider_options: None,
395 }),
396 }
397}
398
399fn durable_tool_result_from_content(content: &Content) -> Option<DurableToolResult> {
400 let Content::Parts(parts) = content else {
401 return None;
402 };
403 let [
404 ContentPart::ToolResult {
405 call_id,
406 is_error,
407 output,
408 durable_content,
409 },
410 ] = parts.as_slice()
411 else {
412 return None;
413 };
414 Some(durable_tool_result_from_part(
415 call_id,
416 output,
417 *is_error,
418 durable_content.as_ref(),
419 ))
420}
421
422fn durable_tool_results_from_content(content: &Content) -> Option<Vec<DurableToolResult>> {
423 let Content::Parts(parts) = content else {
424 return None;
425 };
426 if parts.len() < 2 {
427 return None;
428 }
429 let results = parts
430 .iter()
431 .map(|part| match part {
432 ContentPart::ToolResult {
433 call_id,
434 output,
435 is_error,
436 durable_content,
437 } => Some(durable_tool_result_from_part(
438 call_id,
439 output,
440 *is_error,
441 durable_content.as_ref(),
442 )),
443 _ => None,
444 })
445 .collect::<Option<Vec<_>>>()?;
446 Some(results)
447}
448
449fn durable_tool_result_from_part(
450 call_id: &str,
451 output: &str,
452 is_error: bool,
453 durable_content: Option<&DurableContent>,
454) -> DurableToolResult {
455 match durable_content {
456 Some(content) => DurableToolResult {
457 call_id: call_id.to_owned(),
458 is_error,
459 blocks: content.blocks.clone(),
460 },
461 None => DurableToolResult::text(call_id.to_owned(), output.to_owned(), is_error),
462 }
463}
464
465fn content_from_durable_tool_result(result: &DurableToolResult) -> Result<Content, String> {
466 result.validate().map_err(|error| error.to_string())?;
467 let output = result
468 .blocks
469 .iter()
470 .filter_map(|block| match block {
471 DurableContentBlock::Text { text } => Some(text.as_str()),
472 _ => None,
473 })
474 .collect::<String>();
475 Ok(Content::Parts(vec![ContentPart::ToolResult {
476 call_id: result.call_id.clone().into(),
477 output,
478 is_error: result.is_error,
479 durable_content: Some(DurableContent {
480 blocks: result.blocks.clone(),
481 }),
482 }]))
483}
484
485fn content_from_durable_tool_results(results: &[DurableToolResult]) -> Result<Content, String> {
486 let mut parts = Vec::with_capacity(results.len());
487 for result in results {
488 let Content::Parts(mut result_parts) = content_from_durable_tool_result(result)? else {
489 return Err("durable tool result did not restore to tool content".into());
490 };
491 parts.append(&mut result_parts);
492 }
493 Ok(Content::Parts(parts))
494}
495
496fn content_from_durable(content: &DurableContent) -> Result<Content, String> {
497 let parts = content
498 .blocks
499 .iter()
500 .map(durable_block_to_content_part)
501 .collect::<Result<Vec<_>, _>>()?;
502 if parts.len() == 1 {
503 if let ContentPart::Text { text } = &parts[0] {
504 return Ok(Content::Text(text.clone()));
505 }
506 }
507 Ok(Content::Parts(parts))
508}
509
510fn durable_block_to_content_part(block: &DurableContentBlock) -> Result<ContentPart, String> {
511 match block {
512 DurableContentBlock::Text { text } => Ok(ContentPart::Text { text: text.clone() }),
513 DurableContentBlock::Image {
514 source,
515 media_type,
516 provider_options,
517 } => match source {
518 DurableSource::Url { url } => Ok(ContentPart::Image {
519 source: DurableSource::Url { url: url.clone() },
520 media_type: media_type.clone(),
521 detail: provider_options
522 .as_ref()
523 .and_then(|value| value.get("detail"))
524 .and_then(serde_json::Value::as_str)
525 .map(str::to_string),
526 }),
527 DurableSource::Base64 { data } => Ok(ContentPart::Image {
528 source: DurableSource::Base64 { data: data.clone() },
529 media_type: media_type.clone(),
530 detail: provider_options
531 .as_ref()
532 .and_then(|value| value.get("detail"))
533 .and_then(serde_json::Value::as_str)
534 .map(str::to_string),
535 }),
536 _ => Err("this kernel only restores image url/base64 sources".into()),
537 },
538 DurableContentBlock::Audio {
539 source: DurableSource::Base64 { data },
540 media_type,
541 ..
542 } => Ok(ContentPart::Audio {
543 source: DurableSource::Base64 { data: data.clone() },
544 media_type: media_type
545 .clone()
546 .ok_or_else(|| "audio durable block requires media_type".to_string())?,
547 }),
548 DurableContentBlock::Audio { .. }
549 | DurableContentBlock::File { .. }
550 | DurableContentBlock::Video { .. } => {
551 Err("this kernel content vocabulary cannot restore the durable media source".into())
552 }
553 }
554}
555
556fn workflow_kind_label(state: &WorkflowRuntimeNodeState) -> &'static str {
557 match state.node.kind {
558 crate::orchestration::workflow::NodeKind::Spawn => "spawn",
559 crate::orchestration::workflow::NodeKind::Loop { .. } => "loop",
560 crate::orchestration::workflow::NodeKind::Classify { .. } => "classify",
561 crate::orchestration::workflow::NodeKind::Tournament { .. } => "tournament",
562 crate::orchestration::workflow::NodeKind::Reduce { .. } => "reduce",
563 }
564}
565
566fn workflow_status_label(status: TaskStatus) -> &'static str {
567 match status {
568 TaskStatus::Pending => "pending",
569 TaskStatus::Ready => "ready",
570 TaskStatus::Running => "running",
571 TaskStatus::Completed => "completed",
572 TaskStatus::CompletedPartial => "completed_partial",
573 TaskStatus::Failed => "failed",
574 TaskStatus::SkippedUpstreamFailed => "skipped_upstream_failed",
575 }
576}
577
578fn restore_workflow_status(label: &str) -> Result<TaskStatus, KernelFault> {
579 match label {
580 "pending" => Ok(TaskStatus::Pending),
581 "ready" => Ok(TaskStatus::Ready),
582 "running" => Ok(TaskStatus::Running),
583 "completed" => Ok(TaskStatus::Completed),
584 "completed_partial" => Ok(TaskStatus::CompletedPartial),
585 "failed" => Ok(TaskStatus::Failed),
586 "skipped_upstream_failed" => Ok(TaskStatus::SkippedUpstreamFailed),
587 other => Err(KernelFault::new(
588 KernelFaultCode::CheckpointIncompatible,
589 format!("workflow checkpoint carries unknown node status {other:?}"),
590 )),
591 }
592}
593
594fn agent_role_label(role: AgentRole) -> &'static str {
595 match role {
596 AgentRole::Explore => "explore",
597 AgentRole::Plan => "plan",
598 AgentRole::Implement => "implement",
599 AgentRole::Verify => "verify",
600 AgentRole::Custom => "custom",
601 }
602}
603
604fn restore_agent_role(label: &str) -> Result<AgentRole, KernelFault> {
605 match label {
606 "explore" => Ok(AgentRole::Explore),
607 "plan" => Ok(AgentRole::Plan),
608 "implement" => Ok(AgentRole::Implement),
609 "verify" => Ok(AgentRole::Verify),
610 "custom" => Ok(AgentRole::Custom),
611 other => Err(KernelFault::new(
612 KernelFaultCode::CheckpointIncompatible,
613 format!("child process carries unknown role {other:?}"),
614 )),
615 }
616}
617
618fn agent_isolation_label(isolation: AgentIsolation) -> &'static str {
619 match isolation {
620 AgentIsolation::Shared => "shared",
621 AgentIsolation::ReadOnly => "read_only",
622 AgentIsolation::Worktree => "worktree",
623 AgentIsolation::Remote => "remote",
624 }
625}
626
627fn restore_agent_isolation(label: &str) -> Result<AgentIsolation, KernelFault> {
628 match label {
629 "shared" => Ok(AgentIsolation::Shared),
630 "read_only" => Ok(AgentIsolation::ReadOnly),
631 "worktree" => Ok(AgentIsolation::Worktree),
632 "remote" => Ok(AgentIsolation::Remote),
633 other => Err(KernelFault::new(
634 KernelFaultCode::CheckpointIncompatible,
635 format!("child process carries unknown isolation {other:?}"),
636 )),
637 }
638}
639
640fn context_inheritance_label(inheritance: ContextInheritance) -> &'static str {
641 match inheritance {
642 ContextInheritance::None => "none",
643 ContextInheritance::SystemOnly => "system_only",
644 ContextInheritance::Full => "full",
645 }
646}
647
648fn restore_context_inheritance(label: &str) -> Result<ContextInheritance, KernelFault> {
649 match label {
650 "none" => Ok(ContextInheritance::None),
651 "system_only" => Ok(ContextInheritance::SystemOnly),
652 "full" => Ok(ContextInheritance::Full),
653 other => Err(KernelFault::new(
654 KernelFaultCode::CheckpointIncompatible,
655 format!("child process carries unknown context inheritance {other:?}"),
656 )),
657 }
658}
659
660fn queued_signal_state(queued: &QueuedSignalRuntimeState) -> QueuedSignalState {
661 let signal = &queued.signal;
662 QueuedSignalState {
663 signal_id: super::scalar::SignalId::new(signal.id.as_str())
664 .expect("a canonical runtime signal keeps its branded id"),
665 source: signal_source_label(signal.source).to_string(),
666 signal_type: signal_type_label(signal.signal_type).to_string(),
667 urgency: urgency_label(signal.urgency).to_string(),
668 summary: signal.summary.to_string(),
669 payload: super::scalar::BoundedJson::new(signal.payload.clone())
670 .expect("a canonical signal payload remains bounded"),
671 dedupe_key: signal.dedupe_key.as_ref().map(ToString::to_string),
672 deadline_ms: signal.deadline_ms.map(WireU64::new),
673 coalesce_key: signal.coalesce_key.as_ref().map(ToString::to_string),
674 coalesced_count: signal.coalesced_count,
675 recipient: signal.recipient.as_ref().map(ToString::to_string),
676 timestamp_ms: WireU64::new(signal.timestamp_ms),
677 deadline_escalated: queued.deadline_escalated,
678 dedupe_keys: queued.dedupe_keys.iter().map(ToString::to_string).collect(),
679 }
680}
681
682fn restore_queued_signal(
683 queued: &QueuedSignalState,
684) -> Result<QueuedSignalRuntimeState, KernelFault> {
685 if queued.coalesced_count == 0 {
686 return Err(KernelFault::new(
687 KernelFaultCode::CheckpointIncompatible,
688 format!(
689 "queued signal {} carries a zero coalesced count",
690 queued.signal_id
691 ),
692 ));
693 }
694 Ok(QueuedSignalRuntimeState {
695 signal: RuntimeSignal {
696 id: queued.signal_id.as_str().into(),
697 source: restore_signal_source(&queued.source)?,
698 signal_type: restore_signal_type(&queued.signal_type)?,
699 urgency: restore_urgency(&queued.urgency)?,
700 summary: queued.summary.as_str().into(),
701 payload: queued.payload.get().clone(),
702 dedupe_key: queued.dedupe_key.as_deref().map(Into::into),
703 deadline_ms: queued.deadline_ms.map(WireU64::get),
704 coalesce_key: queued.coalesce_key.as_deref().map(Into::into),
705 coalesced_count: queued.coalesced_count,
706 recipient: queued.recipient.as_deref().map(Into::into),
707 timestamp_ms: queued.timestamp_ms.get(),
708 },
709 deadline_escalated: queued.deadline_escalated,
710 dedupe_keys: queued
711 .dedupe_keys
712 .iter()
713 .map(|key| key.as_str().into())
714 .collect(),
715 })
716}
717
718fn signal_source_label(source: SignalSource) -> &'static str {
719 match source {
720 SignalSource::Cron => "cron",
721 SignalSource::Gateway => "gateway",
722 SignalSource::Heartbeat => "heartbeat",
723 SignalSource::Custom => "custom",
724 }
725}
726
727fn restore_signal_source(label: &str) -> Result<SignalSource, KernelFault> {
728 match label {
729 "cron" => Ok(SignalSource::Cron),
730 "gateway" => Ok(SignalSource::Gateway),
731 "heartbeat" => Ok(SignalSource::Heartbeat),
732 "custom" => Ok(SignalSource::Custom),
733 other => Err(KernelFault::new(
734 KernelFaultCode::CheckpointIncompatible,
735 format!("queued signal carries unknown source {other:?}"),
736 )),
737 }
738}
739
740fn signal_type_label(signal_type: SignalType) -> &'static str {
741 match signal_type {
742 SignalType::Event => "event",
743 SignalType::Job => "job",
744 SignalType::Alert => "alert",
745 }
746}
747
748fn restore_signal_type(label: &str) -> Result<SignalType, KernelFault> {
749 match label {
750 "event" => Ok(SignalType::Event),
751 "job" => Ok(SignalType::Job),
752 "alert" => Ok(SignalType::Alert),
753 other => Err(KernelFault::new(
754 KernelFaultCode::CheckpointIncompatible,
755 format!("queued signal carries unknown type {other:?}"),
756 )),
757 }
758}
759
760fn urgency_label(urgency: Urgency) -> &'static str {
761 match urgency {
762 Urgency::Low => "low",
763 Urgency::Normal => "normal",
764 Urgency::High => "high",
765 Urgency::Critical => "critical",
766 }
767}
768
769fn restore_urgency(label: &str) -> Result<Urgency, KernelFault> {
770 match label {
771 "low" => Ok(Urgency::Low),
772 "normal" => Ok(Urgency::Normal),
773 "high" => Ok(Urgency::High),
774 "critical" => Ok(Urgency::Critical),
775 other => Err(KernelFault::new(
776 KernelFaultCode::CheckpointIncompatible,
777 format!("queued signal carries unknown urgency {other:?}"),
778 )),
779 }
780}
781
782fn restore_scheduler(
784 engine: &mut LoopStateMachine,
785 config: &ResolvedOperationConfig,
786 state: &SchedulerState,
787) -> Result<(), KernelFault> {
788 engine.run_spec = state.run_spec.as_ref().map(agent_run_spec);
789 if let Some(names) = &state.advertised_tool_ids {
790 let unique: std::collections::BTreeSet<&str> = names.iter().map(String::as_str).collect();
791 if unique.len() != names.len() {
792 return Err(KernelFault::new(
793 KernelFaultCode::CheckpointIncompatible,
794 "advertised_tool_ids contains a duplicate tool id",
795 ));
796 }
797 }
798 engine.restore_advertised_tool_ids(state.advertised_tool_ids.clone());
799 engine.turn = state.turn;
800 engine.restore_budget_usage(state.total_tokens.get(), state.rounds_completed);
801 engine.restore_started_at_ms(state.started_at_ms.map(WireU64::get));
802 engine.set_wall_budget(state.wall_budget_ms.map(WireU64::get));
803 engine
804 .restore_entropy_checkpoint_state(crate::scheduler::entropy::EntropyTrackerRuntimeState {
805 window: state
806 .entropy
807 .window
808 .iter()
809 .map(|entry| crate::scheduler::entropy::EntropyTurnRuntimeState {
810 errored_results: entry.errored_results,
811 total_results: entry.total_results,
812 rollbacks: entry.rollbacks,
813 })
814 .collect(),
815 rollbacks_pending: state.entropy.rollbacks_pending,
816 disarmed: state.entropy.disarmed,
817 last_alert_turn: state.entropy.last_alert_turn,
818 })
819 .map_err(|error| {
820 KernelFault::new(
821 KernelFaultCode::CheckpointIncompatible,
822 format!("entropy checkpoint could not be rebuilt: {error}"),
823 )
824 })?;
825
826 let limits = SchedulerBudget {
827 max_tokens: config.execution_policy.max_context_tokens,
828 max_turns: config.execution_policy.max_turns,
829 max_total_tokens: config.execution_policy.max_total_tokens.get(),
830 max_wall_ms: state.wall_budget_ms.map(WireU64::get),
831 };
832 let table = engine.task_table_mut();
833 for task in &state.tasks {
834 let mut tcb = Tcb::root(task.task_id.as_str(), limits.clone());
835 tcb.parent = task
836 .parent_task_id
837 .as_ref()
838 .map(|parent| parent.as_str().into());
839 tcb.state = restore_task_lifecycle(task)?;
840 tcb.runnable_cause = task.runnable_cause;
841 tcb.wait_set = task
842 .wait_set
843 .as_ref()
844 .map(|wait_set| restore_wait_set(&task.task_id, wait_set))
845 .transpose()?;
846 tcb.caps = task.capability_ids.iter().map(|cap| cap.into()).collect();
847 tcb.capabilities = task.capabilities.clone();
848 tcb.supervision = task.supervision.clone();
849 tcb.supervision_events = task.supervision_events.clone();
850 tcb.child_budget_remaining = task.child_budget_remaining;
855 tcb.budget_grant = task.budget_grant.clone();
856 tcb.mailbox = task.mailbox.clone();
857 if let Some(grant) = tcb.budget_grant.as_ref()
858 && (grant.child.as_str() != task.task_id.as_str()
859 || tcb.parent.as_deref() != Some(grant.parent.as_str()))
860 {
861 return Err(KernelFault::new(
862 KernelFaultCode::CheckpointIncompatible,
863 format!(
864 "task {} carries a hierarchical budget grant for parent {} and child {}",
865 task.task_id, grant.parent, grant.child
866 ),
867 ));
868 }
869 tcb.proc = task
870 .process
871 .as_ref()
872 .map(|process| {
873 let result = process
874 .join_result
875 .as_ref()
876 .map(|value| {
877 serde_json::from_value(value.get().clone()).map_err(|error| {
878 KernelFault::new(
879 KernelFaultCode::CheckpointIncompatible,
880 format!(
881 "task {} carries an invalid child join result: {error}",
882 task.task_id
883 ),
884 )
885 })
886 })
887 .transpose()?;
888 if result.as_ref().is_some_and(|result: &SubAgentResult| {
889 result.agent_id.as_str() != task.task_id.as_str()
890 }) {
891 return Err(KernelFault::new(
892 KernelFaultCode::CheckpointIncompatible,
893 format!(
894 "task {} carries a join result for another child",
895 task.task_id
896 ),
897 ));
898 }
899 Ok(ProcInfo {
900 role: restore_agent_role(&process.role)?,
901 isolation: restore_agent_isolation(&process.isolation)?,
902 context_inheritance: restore_context_inheritance(&process.context_inheritance)?,
903 result,
904 })
905 })
906 .transpose()?;
907 tcb.budget = BudgetLedger {
908 limits: limits.clone(),
909 turns: task.turns_used,
910 total_tokens: task.tokens_used.get(),
911 started_at_ms: state.started_at_ms.map(WireU64::get),
912 };
913 table.insert(tcb);
914 }
915 let mut restored_channels = BTreeMap::new();
916 for channel in &state.channels {
917 let id = ChannelId(channel.channel_id.as_str().into());
918 if restored_channels
919 .insert(id, channel.channel.clone())
920 .is_some()
921 {
922 return Err(KernelFault::new(
923 KernelFaultCode::CheckpointIncompatible,
924 format!("duplicate local channel {:?}", channel.channel_id),
925 ));
926 }
927 }
928 table.restore_channels(restored_channels);
929 let mut restored_objects = BTreeMap::new();
930 for object in &state.objects {
931 if table.get(object.owner.as_str()).is_none() {
932 return Err(KernelFault::new(
933 KernelFaultCode::CheckpointIncompatible,
934 format!("object {} names unknown owner {}", object.id, object.owner),
935 ));
936 }
937 if restored_objects.insert(object.id, object.clone()).is_some() {
938 return Err(KernelFault::new(
939 KernelFaultCode::CheckpointIncompatible,
940 format!("duplicate local object {}", object.id),
941 ));
942 }
943 }
944 table.restore_objects(restored_objects);
945 table.rebuild_children();
949 table.rebuild_wait_index();
951
952 let queued = state
953 .queued_signals
954 .iter()
955 .map(restore_queued_signal)
956 .collect::<Result<Vec<_>, _>>()?;
957 engine
958 .restore_signal_checkpoint_state(SignalRouterRuntimeState {
959 queued,
960 seen_order: state
961 .signal_dedupe_keys
962 .iter()
963 .map(|key| key.as_str().into())
964 .collect(),
965 })
966 .map_err(|error| {
967 KernelFault::new(
968 KernelFaultCode::CheckpointIncompatible,
969 format!("signal checkpoint could not be rebuilt: {error}"),
970 )
971 })?;
972
973 if let Some(workflow) = &state.workflow {
974 let wire_spec = WireSpec {
975 name: String::new(),
976 nodes: workflow
977 .nodes
978 .iter()
979 .map(|node| WireNode {
980 node_id: node.node_id.clone(),
981 task: node.task.clone(),
982 depends_on: node.depends_on.clone(),
983 run_spec: node.run_spec.clone(),
984 })
985 .collect(),
986 };
987 let core_spec = build_core_spec(&wire_spec).map_err(|fault| {
988 KernelFault::new(KernelFaultCode::CheckpointIncompatible, fault.message)
989 })?;
990 let runtime_states: Result<Vec<_>, KernelFault> = workflow
991 .nodes
992 .iter()
993 .enumerate()
994 .zip(core_spec.nodes.iter())
995 .map(|((index, node), core)| {
996 if node.kind != "spawn" {
997 return Err(KernelFault::new(
998 KernelFaultCode::CheckpointIncompatible,
999 format!(
1000 "workflow node {} carries unsupported checkpoint kind {:?}",
1001 node.node_id, node.kind
1002 ),
1003 ));
1004 }
1005 let result = engine
1006 .task_table()
1007 .get(&crate::orchestration::workflow::node_agent_id(index))
1008 .and_then(|task| task.proc.as_ref())
1009 .and_then(|process| process.result.as_ref())
1010 .map(|result| result.result.clone());
1011 Ok(WorkflowRuntimeNodeState {
1012 node: core.clone(),
1013 status: restore_workflow_status(&node.status)?,
1014 result,
1015 active_agent_id: node.active_agent_id.clone(),
1016 iterations_completed: node.iterations_completed as usize,
1017 })
1018 })
1019 .collect();
1020 let run = crate::orchestration::workflow::WorkflowRun::restore_from_checkpoint(
1021 &core_spec,
1022 &runtime_states?,
1023 )
1024 .map_err(|error| {
1025 KernelFault::new(
1026 KernelFaultCode::CheckpointIncompatible,
1027 format!("workflow checkpoint could not be rebuilt: {error}"),
1028 )
1029 })?;
1030 engine.restore_checkpoint_workflow(run);
1031 }
1032 Ok(())
1033}
1034
1035fn restore_task_lifecycle(task: &TaskControlState) -> Result<TaskLifecycle, KernelFault> {
1036 let lifecycle = match task.lifecycle.as_str() {
1037 "pending_launch" => TaskLifecycle::PendingLaunch,
1038 "starting" => TaskLifecycle::Starting,
1039 "ready" => TaskLifecycle::Ready,
1040 "running" => TaskLifecycle::Running,
1041 "suspended" => TaskLifecycle::Suspended,
1042 "done" => {
1043 let label = task.termination.as_deref().ok_or_else(|| {
1044 incompatible(format!(
1045 "task {} is done but the checkpoint does not say why; a finished task without \
1046 its termination reason is not restorable",
1047 task.task_id
1048 ))
1049 })?;
1050 TaskLifecycle::Done(termination_from_label(label).ok_or_else(|| {
1051 incompatible(format!(
1052 "task {} names termination reason {label:?}, which this kernel does not know",
1053 task.task_id
1054 ))
1055 })?)
1056 }
1057 other => {
1058 return Err(incompatible(format!(
1059 "task {} names lifecycle {other:?}, which this kernel does not know",
1060 task.task_id
1061 )));
1062 }
1063 };
1064 Ok(lifecycle)
1065}
1066
1067fn project_wait_set(wait_set: &DurableWaitSet) -> TaskWaitSetState {
1068 TaskWaitSetState {
1069 mode: match wait_set.mode {
1070 WaitMode::Any => "any",
1071 WaitMode::All => "all",
1072 }
1073 .to_string(),
1074 conditions: wait_set
1075 .conditions
1076 .iter()
1077 .map(|condition| match condition {
1078 WaitCondition::Effect(effect_id) => TaskWaitConditionState::Effect {
1079 effect_id: effect_id.clone(),
1080 },
1081 WaitCondition::Child(task_id) => TaskWaitConditionState::Child {
1082 task_id: TaskId::new(task_id.as_str())
1083 .expect("an internal task id is a legal branded ref"),
1084 },
1085 WaitCondition::Children(task_ids) => TaskWaitConditionState::Children {
1086 task_ids: task_ids
1087 .iter()
1088 .map(|task_id| {
1089 TaskId::new(task_id.as_str())
1090 .expect("an internal task id is a legal branded ref")
1091 })
1092 .collect(),
1093 },
1094 WaitCondition::Approval(ApprovalId(id)) => TaskWaitConditionState::Approval {
1095 approval_id: id.to_string(),
1096 },
1097 WaitCondition::Signal(SignalFilter(filter)) => TaskWaitConditionState::Signal {
1098 filter: filter.to_string(),
1099 },
1100 WaitCondition::Timer(LogicalDeadline(deadline_ms)) => {
1101 TaskWaitConditionState::Timer {
1102 deadline_ms: WireU64::new(*deadline_ms),
1103 }
1104 }
1105 WaitCondition::Channel(ChannelId(id)) => TaskWaitConditionState::Channel {
1106 channel_id: id.to_string(),
1107 },
1108 WaitCondition::Resource(ResourceKey(key)) => TaskWaitConditionState::Resource {
1109 resource_key: key.to_string(),
1110 },
1111 WaitCondition::External(SubscriptionId(id)) => TaskWaitConditionState::External {
1112 subscription_id: id.to_string(),
1113 },
1114 })
1115 .collect(),
1116 satisfied: wait_set
1117 .satisfied
1118 .iter()
1119 .map(|index| *index as u32)
1120 .collect(),
1121 }
1122}
1123
1124fn restore_wait_set(
1125 task_id: &TaskId,
1126 state: &TaskWaitSetState,
1127) -> Result<DurableWaitSet, KernelFault> {
1128 let mode = match state.mode.as_str() {
1129 "any" => WaitMode::Any,
1130 "all" => WaitMode::All,
1131 other => {
1132 return Err(incompatible(format!(
1133 "task {task_id} wait set names mode {other:?}, which this kernel does not know"
1134 )));
1135 }
1136 };
1137 if state.conditions.is_empty() {
1138 return Err(incompatible(format!(
1139 "task {task_id} carries an empty durable WaitSet"
1140 )));
1141 }
1142 let conditions = state
1143 .conditions
1144 .iter()
1145 .map(|condition| match condition {
1146 TaskWaitConditionState::Effect { effect_id } => {
1147 WaitCondition::Effect(effect_id.clone())
1148 }
1149 TaskWaitConditionState::Child { task_id } => {
1150 WaitCondition::Child(task_id.as_str().into())
1151 }
1152 TaskWaitConditionState::Children { task_ids } => WaitCondition::Children(
1153 task_ids
1154 .iter()
1155 .map(|task_id| task_id.as_str().into())
1156 .collect(),
1157 ),
1158 TaskWaitConditionState::Approval { approval_id } => {
1159 WaitCondition::Approval(ApprovalId(approval_id.as_str().into()))
1160 }
1161 TaskWaitConditionState::Signal { filter } => {
1162 WaitCondition::Signal(SignalFilter(filter.as_str().into()))
1163 }
1164 TaskWaitConditionState::Timer { deadline_ms } => {
1165 WaitCondition::Timer(LogicalDeadline(deadline_ms.get()))
1166 }
1167 TaskWaitConditionState::Channel { channel_id } => {
1168 WaitCondition::Channel(ChannelId(channel_id.as_str().into()))
1169 }
1170 TaskWaitConditionState::Resource { resource_key } => {
1171 WaitCondition::Resource(ResourceKey(resource_key.as_str().into()))
1172 }
1173 TaskWaitConditionState::External { subscription_id } => {
1174 WaitCondition::External(SubscriptionId(subscription_id.as_str().into()))
1175 }
1176 })
1177 .collect::<Vec<_>>();
1178 let mut satisfied = BTreeSet::new();
1179 for index in &state.satisfied {
1180 let index = *index as usize;
1181 if index >= conditions.len() || !satisfied.insert(index) {
1182 return Err(incompatible(format!(
1183 "task {task_id} carries invalid satisfied WaitSet index {index}"
1184 )));
1185 }
1186 }
1187 Ok(DurableWaitSet {
1188 mode,
1189 conditions,
1190 satisfied,
1191 })
1192}
1193
1194fn termination_from_label(label: &str) -> Option<TerminationReason> {
1195 Some(match label {
1196 "completed" => TerminationReason::Completed,
1197 "max_turns" => TerminationReason::MaxTurns,
1198 "token_budget" => TerminationReason::TokenBudget,
1199 "timeout" => TerminationReason::Timeout,
1200 "user_abort" => TerminationReason::UserAbort,
1201 "error" => TerminationReason::Error,
1202 "milestone_exceeded" => TerminationReason::MilestoneExceeded,
1203 "context_overflow" => TerminationReason::ContextOverflow,
1204 "no_progress" => TerminationReason::NoProgress,
1205 _ => return None,
1206 })
1207}
1208
1209fn restore_context_vm(
1215 engine: &mut LoopStateMachine,
1216 state: &ContextVmState,
1217) -> Result<(), KernelFault> {
1218 let ctx = &mut engine.ctx;
1219 for entry in &state.messages {
1220 let message = restore_message(&entry.role, &entry.body, &entry.tool_calls)?;
1221 match entry.partition {
1222 MessagePartition::System => ctx.partitions.system.push(message, entry.tokens),
1223 MessagePartition::History => ctx.partitions.history.push(message, entry.tokens),
1224 }
1225 }
1226 for slot in &state.knowledge {
1227 let message = restore_message(&slot.role, &slot.body, &slot.tool_calls)?;
1228 ctx.partitions.knowledge.push_entry(
1229 slot.key.as_deref().map(Into::into),
1230 message,
1231 slot.tokens,
1232 slot.pinned,
1233 );
1234 if let Some(entry) = ctx.partitions.knowledge.entries.last_mut() {
1235 entry.evict_at_boundary = slot.evict_at_boundary;
1236 entry.use_count = slot.use_count;
1237 entry.last_used_step = slot.last_used_step;
1238 entry.pending = slot
1239 .pending
1240 .as_ref()
1241 .map(|pending| {
1242 restore_message(&pending.role, &pending.body, &pending.tool_calls)
1243 .map(|message| Box::new((message, pending.tokens)))
1244 })
1245 .transpose()?;
1246 }
1247 }
1248 ctx.partitions.system.measurements = state.system_measurements.clone();
1249 ctx.partitions.history.measurements = state.history_measurements.clone();
1250 ctx.restore_knowledge_checkpoint_state(
1251 state.knowledge_reference_step,
1252 state.knowledge_budget_warned,
1253 );
1254 ctx.partitions.signals = state.signals.clone();
1255 ctx.partitions.task_state = restore_task_state(&state.task_state);
1256 ctx.restore_state_generation(state.state_generation);
1257 ctx.last_activity_ms = state.last_activity_ms.get();
1258 ctx.last_compact_ms = state.last_compact_ms.map(WireU64::get);
1259 ctx.active_skills = state
1260 .active_skills
1261 .iter()
1262 .map(|lease| (lease.skill.as_str().into(), lease.lease_until_turn))
1263 .collect();
1264
1265 for handle in &state.handles {
1266 ctx.handles.insert(Handle {
1267 id: handle.handle_id,
1268 kind: restore_handle_kind(&handle.kind)?,
1269 residency: restore_residency(handle)?,
1270 tokens: handle.tokens,
1271 source: handle.source.as_deref().map(Into::into),
1272 });
1273 }
1274 ctx.restore_next_handle_id(state.next_handle_id);
1275 if !ctx.restore_frozen_history_len(state.frozen_history_len as usize) {
1276 return Err(incompatible(format!(
1277 "the checkpoint freezes {} history messages but restores only {}",
1278 state.frozen_history_len,
1279 ctx.partitions.history.messages.len()
1280 )));
1281 }
1282 Ok(())
1283}
1284
1285fn restore_message(
1286 role: &str,
1287 body: &StoredMessageBody,
1288 tool_calls: &[LogicalToolCall],
1289) -> Result<CoreMessage, KernelFault> {
1290 let role = role_from_label(role)
1291 .ok_or_else(|| incompatible(format!("the checkpoint carries message role {role:?}")))?;
1292 let content = match body {
1293 StoredMessageBody::Inline(inline) => message_content(
1294 inline.text.clone(),
1295 inline.tool_call_id.as_deref(),
1296 inline.is_error,
1297 ),
1298 StoredMessageBody::Reference(reference) => message_content(
1299 reference.preview.clone(),
1300 reference.tool_call_id.as_deref(),
1301 reference.is_error,
1302 ),
1303 StoredMessageBody::Structured(structured) => {
1304 if !structured.durable_tool_results.is_empty() {
1305 if structured.durable_content.is_some() {
1306 return Err(incompatible(
1307 "the checkpoint durable tool results must not carry another body form"
1308 .to_string(),
1309 ));
1310 }
1311 content_from_durable_tool_results(&structured.durable_tool_results).map_err(|error| incompatible(format!(
1312 "the checkpoint carries durable tool results this runtime cannot restore: {error}"
1313 )))?
1314 } else if let Some(content) = &structured.durable_content {
1315 content.validate().map_err(|error| {
1316 incompatible(format!(
1317 "the checkpoint carries invalid durable content: {error}"
1318 ))
1319 })?;
1320 content_from_durable(content).map_err(|error| incompatible(format!(
1321 "the checkpoint carries durable content this runtime cannot restore: {error}"
1322 )))?
1323 } else {
1324 return Err(incompatible(
1325 "the checkpoint structured message body has no content".to_string(),
1326 ));
1327 }
1328 }
1329 };
1330 Ok(CoreMessage {
1331 role,
1332 content,
1333 tool_calls: tool_calls
1334 .iter()
1335 .map(|call| {
1336 Ok(crate::types::message::ToolCall {
1337 id: call.call_id.as_str().into(),
1338 name: call.name.as_str().into(),
1339 arguments: serde_json::from_str(&call.arguments).map_err(|error| {
1340 incompatible(format!(
1341 "tool call {} carries arguments that do not decode: {error}",
1342 call.call_id
1343 ))
1344 })?,
1345 })
1346 })
1347 .collect::<Result<Vec<_>, KernelFault>>()?,
1348 })
1349}
1350
1351fn restore_handle_kind(label: &str) -> Result<HandleKind, KernelFault> {
1352 Ok(match label {
1353 "tool_result" => HandleKind::ToolResult,
1354 "memory_page" => HandleKind::MemoryPage,
1355 "knowledge_entry" => HandleKind::KnowledgeEntry,
1356 "sub_agent_join" => HandleKind::SubAgentJoin,
1357 other => {
1358 return Err(incompatible(format!(
1359 "the checkpoint carries handle kind {other:?}, which this kernel does not know"
1360 )));
1361 }
1362 })
1363}
1364
1365fn restore_residency(handle: &HandleState) -> Result<Residency, KernelFault> {
1366 let missing = |what: &str| {
1367 incompatible(format!(
1368 "handle {} is {} but carries no {what}",
1369 handle.handle_id, handle.residency
1370 ))
1371 };
1372 Ok(match handle.residency.as_str() {
1373 "resident" => Residency::Resident,
1374 "collapsed" => Residency::Collapsed,
1375 "external" => Residency::External {
1376 payload_ref: handle
1377 .payload_ref
1378 .clone()
1379 .ok_or_else(|| missing("locator"))?,
1380 digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
1381 original_size: handle
1382 .original_size
1383 .ok_or_else(|| missing("original size"))?
1384 .get(),
1385 },
1386 "paged_out" => Residency::PagedOut {
1387 payload_ref: handle
1388 .payload_ref
1389 .clone()
1390 .ok_or_else(|| missing("locator"))?,
1391 digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
1392 },
1393 other => {
1394 return Err(incompatible(format!(
1395 "the checkpoint carries residency {other:?}, which this kernel does not know"
1396 )));
1397 }
1398 })
1399}
1400
1401fn incompatible(message: String) -> KernelFault {
1402 KernelFault::new(KernelFaultCode::CheckpointIncompatible, message)
1403}
1404
1405fn project_task_state(state: &TaskState) -> LogicalTaskState {
1406 LogicalTaskState {
1407 goal: state.goal.clone(),
1408 criteria: state.criteria.clone(),
1409 plan: state
1410 .plan
1411 .iter()
1412 .map(|step| LogicalPlanStep {
1413 label: step.label.clone(),
1414 done: step.done,
1415 })
1416 .collect(),
1417 current_step: state.current_step.map(|index| index as u32),
1418 progress: state.progress.clone(),
1419 scratchpad: state.scratchpad.clone(),
1420 blocked_on: state.blocked_on.clone(),
1421 directives: state.directives.clone(),
1422 preserved_refs: state.preserved_refs.clone(),
1423 recent_actions: state.recent_actions.clone(),
1424 compression_log: state
1425 .compression_log
1426 .iter()
1427 .map(|entry| LogicalCompressionEntry {
1428 action: entry.action.clone(),
1429 summary: entry.summary.clone(),
1430 })
1431 .collect(),
1432 compression_log_dropped: WireU64::new(state.compression_log_dropped),
1433 }
1434}
1435
1436fn restore_task_state(state: &LogicalTaskState) -> TaskState {
1437 TaskState {
1438 goal: state.goal.clone(),
1439 criteria: state.criteria.clone(),
1440 plan: state
1441 .plan
1442 .iter()
1443 .map(|step| PlanStep {
1444 label: step.label.clone(),
1445 done: step.done,
1446 })
1447 .collect(),
1448 current_step: state.current_step.map(|index| index as usize),
1449 progress: state.progress.clone(),
1450 scratchpad: state.scratchpad.clone(),
1451 blocked_on: state.blocked_on.clone(),
1452 directives: state.directives.clone(),
1453 preserved_refs: state.preserved_refs.clone(),
1454 recent_actions: state.recent_actions.clone(),
1455 compression_log: state
1456 .compression_log
1457 .iter()
1458 .map(|entry| CompressionEntry {
1459 action: entry.action.clone(),
1460 summary: entry.summary.clone(),
1461 })
1462 .collect(),
1463 compression_log_dropped: state.compression_log_dropped.get(),
1464 }
1465}
1466
1467fn authority(message: &str) -> SyscallRefusal {
1468 SyscallRefusal::Fault(KernelFault::new(
1469 KernelFaultCode::InvalidAuthority,
1470 message.to_string(),
1471 ))
1472}
1473
1474fn denial_reason(disposition: &Disposition, fallback: &str) -> String {
1475 match disposition {
1476 Disposition::Deny { stage, reason } => format!("{stage}: {reason}"),
1477 Disposition::RateLimited { retry_after_ms } => {
1478 format!("rate limited; retry after {retry_after_ms}ms")
1479 }
1480 Disposition::Gate { reason, .. } => format!("awaiting approval: {reason}"),
1481 Disposition::Defer { slot } => format!("deferred at slot {slot}"),
1482 Disposition::Allow => fallback.to_string(),
1483 }
1484}
1485
1486#[derive(Debug, Clone, PartialEq)]
1488struct AuthoredMemoryWrite {
1489 binding_id: MemoryBindingId,
1490 name: String,
1491 kind: WireMemoryKind,
1492 size_bytes: u32,
1493}
1494
1495#[derive(Debug, Clone, PartialEq)]
1497struct AuthoredMemoryQuery {
1498 binding_id: MemoryBindingId,
1499 text: String,
1500 requested_k: u32,
1501}
1502
1503#[derive(Debug, Clone, PartialEq)]
1505struct PendingPayloadLoad {
1506 handle_id: String,
1509 digest: String,
1512 original_size: Option<u64>,
1515}
1516
1517#[derive(Debug, Default)]
1519struct SyscallOutcome {
1520 effects: Vec<KernelEffect>,
1521 focus: Option<ExecutionFocus>,
1523 needs_workflow_round: bool,
1526 ack: Option<String>,
1528}
1529
1530pub struct CanonicalOperationDriver {
1541 engine: Option<LoopStateMachine>,
1542 root_kind: Option<RootKind>,
1543 focus: Option<ExecutionFocus>,
1544 workflow_id: Option<WorkflowId>,
1545 node_ids: Vec<NodeId>,
1548 workflow_nodes: Vec<WireNode>,
1551 attempts: BTreeMap<String, AttemptId>,
1557 provider_calls: BTreeMap<EffectId, PendingProviderCall>,
1560 pending_memory_writes: BTreeMap<EffectId, AuthoredMemoryWrite>,
1565 pending_memory_queries: BTreeMap<EffectId, AuthoredMemoryQuery>,
1567 pending_payload_loads: BTreeMap<EffectId, PendingPayloadLoad>,
1571 consumed_calls: BTreeSet<String>,
1574 policy: Option<LivePolicyState>,
1579 loaded_contract_id: Option<String>,
1587 staged: Option<StagedFocus>,
1588 poison: Option<KernelFault>,
1589}
1590
1591impl Default for CanonicalOperationDriver {
1592 fn default() -> Self {
1593 Self::new()
1594 }
1595}
1596
1597impl CanonicalOperationDriver {
1598 pub fn new() -> Self {
1599 Self {
1600 engine: None,
1601 root_kind: None,
1602 focus: None,
1603 workflow_id: None,
1604 node_ids: Vec::new(),
1605 workflow_nodes: Vec::new(),
1606 attempts: BTreeMap::new(),
1607 provider_calls: BTreeMap::new(),
1608 pending_memory_writes: BTreeMap::new(),
1609 pending_memory_queries: BTreeMap::new(),
1610 pending_payload_loads: BTreeMap::new(),
1611 consumed_calls: BTreeSet::new(),
1612 policy: None,
1613 loaded_contract_id: None,
1614 staged: None,
1615 poison: None,
1616 }
1617 }
1618
1619 pub fn root_kind(&self) -> Option<RootKind> {
1623 self.root_kind
1624 }
1625
1626 pub fn focus(&self) -> Option<&ExecutionFocus> {
1628 self.focus.as_ref()
1629 }
1630
1631 pub fn workflow_id(&self) -> Option<&WorkflowId> {
1632 self.workflow_id.as_ref()
1633 }
1634
1635 pub fn attempt_id(&self, task_id: &str) -> Option<&AttemptId> {
1640 self.attempts.get(task_id)
1641 }
1642
1643 pub fn poison(&self) -> Option<&KernelFault> {
1644 self.poison.as_ref()
1645 }
1646
1647 pub fn engine(&self) -> Option<&LoopStateMachine> {
1649 self.engine.as_ref()
1650 }
1651
1652 pub fn lifecycle(&self) -> OperationLifecycle {
1655 match (self.engine.is_some(), self.root_kind) {
1656 (false, _) => OperationLifecycle::Created,
1657 (true, None) => OperationLifecycle::Configured,
1658 (true, Some(_)) => OperationLifecycle::Running,
1659 }
1660 }
1661}
1662
1663mod continuation;
1664mod effects;
1665mod events;
1666mod planning;
1667mod projection;
1668mod provider;
1669mod syscall;
1670
1671fn root_task_id() -> TaskId {
1676 TaskId::new(ROOT_TASK_ID).expect("the root task id is a legal branded ref")
1677}
1678
1679pub const SYSCALL_TOOL_NAMES: &[&str] = &[
1697 "start_workflow",
1698 "submit_workflow_nodes",
1699 "skill",
1700 "update_plan",
1701 crate::context::manager::MEMORY_TOOL_NAME,
1702 crate::context::manager::READ_RESULT_TOOL_NAME,
1703 "send_message",
1704 "publish_channel",
1705 "receive_mailbox",
1706 "receive_channel",
1707 "read_object",
1708];
1709
1710fn is_syscall_tool(name: &str) -> bool {
1711 SYSCALL_TOOL_NAMES.contains(&name)
1712}
1713
1714fn decode_syscall(call: &WireToolCall) -> Result<SyscallRequest, SyscallRejection> {
1719 let arguments = call.arguments.get().clone();
1720 let name: &'static str = SYSCALL_TOOL_NAMES
1721 .iter()
1722 .copied()
1723 .find(|known| *known == call.name.as_str())
1724 .expect("only recognised syscall tools reach the decoder");
1725
1726 fn decode<T: serde::de::DeserializeOwned>(
1727 name: &'static str,
1728 arguments: serde_json::Value,
1729 ) -> Result<T, SyscallRejection> {
1730 serde_json::from_value(arguments)
1731 .map_err(|error| SyscallRejection::new(name, format!("malformed arguments: {error}")))
1732 }
1733
1734 match name {
1735 "start_workflow" => Ok(SyscallRequest::SubmitWorkflow(
1736 super::syscall::SubmitWorkflowRequest {
1737 spec: decode(name, arguments)?,
1738 },
1739 )),
1740 "submit_workflow_nodes" => {
1741 #[derive(serde::Deserialize)]
1742 struct Args {
1743 nodes: Vec<WireNode>,
1744 }
1745 let args: Args = decode(name, arguments)?;
1746 Ok(SyscallRequest::AppendWorkflowNodes(
1747 super::syscall::AppendWorkflowNodesRequest { nodes: args.nodes },
1748 ))
1749 }
1750 "skill" => {
1751 #[derive(serde::Deserialize)]
1752 struct Args {
1753 name: String,
1754 #[serde(default)]
1755 lease_turns: Option<u32>,
1756 }
1757 let args: Args = decode(name, arguments)?;
1758 Ok(SyscallRequest::ActivateSkill(
1759 super::syscall::ActivateSkillRequest {
1760 name: args.name,
1761 lease_turns: args.lease_turns,
1762 },
1763 ))
1764 }
1765 "update_plan" => Ok(SyscallRequest::UpdateTask(
1766 super::syscall::UpdateTaskRequest {
1767 update: decode(name, arguments)?,
1768 },
1769 )),
1770 crate::context::manager::MEMORY_TOOL_NAME => {
1771 #[derive(serde::Deserialize)]
1772 struct Args {
1773 #[serde(default)]
1774 query: String,
1775 #[serde(default)]
1776 kinds: Vec<WireMemoryKind>,
1777 #[serde(default)]
1778 top_k: Option<u32>,
1779 }
1780 let args: Args = decode(name, arguments)?;
1781 Ok(SyscallRequest::RequestMemoryQuery(
1782 super::syscall::RequestMemoryQueryRequest {
1783 query: super::syscall::MemoryQueryProposal {
1784 text: args.query,
1785 kinds: args.kinds,
1786 limit: args.top_k,
1787 },
1788 },
1789 ))
1790 }
1791 crate::context::manager::READ_RESULT_TOOL_NAME => {
1792 #[derive(serde::Deserialize)]
1793 struct Args {
1794 call_id: String,
1795 }
1796 let args: Args = decode(name, arguments)?;
1797 let handle_id = super::scalar::HandleId::new(args.call_id).map_err(|error| {
1798 SyscallRejection::new(name, format!("malformed handle: {}", error.message))
1799 })?;
1800 Ok(SyscallRequest::PageIn(super::syscall::PageInRequest {
1801 handle_id,
1802 }))
1803 }
1804 "send_message" => Ok(SyscallRequest::SendMessage(decode(name, arguments)?)),
1805 "publish_channel" => Ok(SyscallRequest::PublishChannel(decode(name, arguments)?)),
1806 "receive_mailbox" => Ok(SyscallRequest::ReceiveMailbox(decode(name, arguments)?)),
1807 "receive_channel" => Ok(SyscallRequest::ReceiveChannel(decode(name, arguments)?)),
1808 "read_object" => Ok(SyscallRequest::ReadObject(decode(name, arguments)?)),
1809 other => unreachable!("unrecognised syscall tool {other}"),
1810 }
1811}
1812
1813fn causation_task(causation: &SyscallCausation) -> TaskId {
1815 match causation {
1816 SyscallCausation::ProviderTool(provider) => provider.task_id.clone(),
1817 SyscallCausation::ChildAttempt(child) => child.task_id.clone(),
1818 }
1819}
1820
1821fn privileged_family(request: &SyscallRequest) -> Option<&'static str> {
1831 match request {
1832 SyscallRequest::SubmitWorkflow(_) | SyscallRequest::AppendWorkflowNodes(_) => {
1833 Some("workflow")
1834 }
1835 SyscallRequest::RequestMemoryWrite(_) | SyscallRequest::RequestMemoryQuery(_) => {
1836 Some("memory")
1837 }
1838 SyscallRequest::ActivateSkill(_) => Some("capability"),
1839 SyscallRequest::SendMessage(_) | SyscallRequest::PublishChannel(_) => Some("ipc"),
1840 SyscallRequest::UpdateTask(_)
1841 | SyscallRequest::PageIn(_)
1842 | SyscallRequest::ReceiveMailbox(_)
1843 | SyscallRequest::ReceiveChannel(_)
1844 | SyscallRequest::ReadObject(_) => None,
1845 }
1846}
1847
1848fn core_task_update(update: &WireTaskUpdate) -> crate::context::task_state::TaskUpdate {
1849 crate::context::task_state::TaskUpdate {
1850 plan: update.plan.clone(),
1851 current_step: update.current_step.map(|step| step as usize),
1852 progress: update.progress.clone(),
1853 scratchpad: update.scratchpad.clone(),
1854 blocked_on: update.blocked_on.clone(),
1855 preserved_refs: update.preserved_refs.clone(),
1856 directives: update.directives.clone(),
1857 }
1858}
1859
1860fn mint_effect_id(operation_id: &OperationId, step_seq: WireU64, index: u32) -> EffectId {
1861 EffectId::new(format!("{operation_id}:step:{step_seq}:effect:{index}"))
1862 .expect("an operation-scoped effect id is always a legal branded ref")
1863}
1864
1865fn mint_workflow_id(operation_id: &OperationId, step_seq: WireU64) -> WorkflowId {
1866 WorkflowId::new(format!("{operation_id}:workflow:{step_seq}"))
1867 .expect("an operation-scoped workflow id is always a legal branded ref")
1868}
1869
1870fn parse_node_index(agent_id: &str) -> Option<usize> {
1872 let rest = agent_id.strip_prefix("wf-node")?;
1873 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
1874 digits.parse().ok()
1875}
1876
1877fn wire_node_ids(spec: &WireSpec) -> Vec<NodeId> {
1878 spec.nodes.iter().map(|node| node.node_id.clone()).collect()
1879}
1880
1881fn build_core_spec(spec: &WireSpec) -> Result<CoreWorkflowSpec, KernelFault> {
1884 let mut index_of: BTreeMap<&str, usize> = BTreeMap::new();
1885 for (index, node) in spec.nodes.iter().enumerate() {
1886 if index_of.insert(node.node_id.as_str(), index).is_some() {
1887 return Err(KernelFault::new(
1888 KernelFaultCode::InvalidConfig,
1889 format!(
1890 "workflow node id {:?} appears twice; node identity is unique within a DAG",
1891 node.node_id
1892 ),
1893 ));
1894 }
1895 }
1896 let mut nodes = Vec::with_capacity(spec.nodes.len());
1897 for node in &spec.nodes {
1898 let role = node
1899 .run_spec
1900 .as_ref()
1901 .and_then(|spec| spec.role)
1902 .map_or(AgentRole::Custom, core_role);
1903 let mut core = CoreWorkflowNode::new(runtime_task(&node.task), role);
1904 if let Some(isolation) = node.run_spec.as_ref().and_then(|spec| spec.isolation) {
1905 core = core.with_isolation(core_isolation(isolation));
1906 }
1907 if let Some(inheritance) = node
1908 .run_spec
1909 .as_ref()
1910 .and_then(|spec| spec.context_inheritance)
1911 {
1912 core.context_inheritance = core_context_inheritance(inheritance);
1913 }
1914 if let Some(metadata) = node
1915 .run_spec
1916 .as_ref()
1917 .map(|spec| spec.metadata.get())
1918 .and_then(serde_json::Value::as_object)
1919 {
1920 if let Some(model_hint) = metadata
1921 .get("model_hint")
1922 .and_then(serde_json::Value::as_str)
1923 {
1924 core = core.with_model_hint(model_hint);
1925 }
1926 if let Some(output_schema) = metadata.get("output_schema") {
1927 core = core.with_output_schema(output_schema.clone());
1928 }
1929 if let Some(requested) = metadata.get("requested_capabilities") {
1935 let capabilities: Vec<crate::types::capability::Capability> =
1936 serde_json::from_value(requested.clone()).map_err(|error| {
1937 KernelFault::new(
1938 KernelFaultCode::InvalidConfig,
1939 format!(
1940 "workflow node {:?} metadata.requested_capabilities is malformed: {error}",
1941 node.node_id
1942 ),
1943 )
1944 })?;
1945 core = core.with_requested_capabilities(capabilities);
1946 }
1947 if let Some(requested) = metadata.get("requested_budget") {
1950 let budget: crate::scheduler::budget_grant::ResourceBudget =
1951 serde_json::from_value(requested.clone()).map_err(|error| {
1952 KernelFault::new(
1953 KernelFaultCode::InvalidConfig,
1954 format!(
1955 "workflow node {:?} metadata.requested_budget is malformed: {error}",
1956 node.node_id
1957 ),
1958 )
1959 })?;
1960 core = core.with_requested_budget(budget);
1961 }
1962 if let Some(factors) = metadata.get("scheduling_factors") {
1963 let factors: crate::orchestration::task_graph::SchedulingFactors =
1964 serde_json::from_value(factors.clone()).map_err(|error| {
1965 KernelFault::new(
1966 KernelFaultCode::InvalidConfig,
1967 format!(
1968 "workflow node {:?} metadata.scheduling_factors is malformed: {error}",
1969 node.node_id
1970 ),
1971 )
1972 })?;
1973 core = core.with_scheduling_factors(factors);
1974 }
1975 }
1976 let mut depends_on = Vec::with_capacity(node.depends_on.len());
1977 for dependency in &node.depends_on {
1978 let Some(&index) = index_of.get(dependency.as_str()) else {
1979 return Err(KernelFault::new(
1980 KernelFaultCode::InvalidConfig,
1981 format!(
1982 "workflow node {:?} depends on {:?}, which this DAG does not declare",
1983 node.node_id, dependency
1984 ),
1985 ));
1986 };
1987 depends_on.push(index);
1988 }
1989 nodes.push(core.with_depends_on(depends_on));
1990 }
1991 let core = CoreWorkflowSpec::new(nodes);
1992 core.validate()
1993 .map_err(|error| KernelFault::new(KernelFaultCode::InvalidConfig, error.to_string()))?;
1994 Ok(core)
1995}
1996
1997fn runtime_task(task: &LogicalTask) -> RuntimeTask {
1998 RuntimeTask {
1999 goal: task.goal.clone(),
2000 criteria: task.criteria.clone(),
2001 metadata: task.metadata.get().clone(),
2002 lane: task.lane.as_ref().map(TaskLane::new).unwrap_or_default(),
2003 }
2004}
2005
2006fn agent_run_spec(spec: &LogicalAgentSpec) -> AgentRunSpec {
2008 AgentRunSpec {
2009 identity: AgentIdentity::new(ROOT_TASK_ID, NO_HOST_SESSION),
2010 role: spec.role.map_or(AgentRole::Custom, core_role),
2011 isolation: spec
2012 .isolation
2013 .map_or(AgentIsolation::Shared, core_isolation),
2014 goal: spec.goal.clone(),
2015 verification_contract_id: spec.verification_contract_id.as_deref().map(Into::into),
2016 capability_filter: AgentCapabilityFilter {
2017 allowed_kinds: spec
2018 .capability_filter
2019 .allowed_kinds
2020 .iter()
2021 .copied()
2022 .map(core_capability_kind)
2023 .collect(),
2024 allowed_ids: spec
2025 .capability_filter
2026 .allowed_ids
2027 .iter()
2028 .map(|id| id.as_str().into())
2029 .collect(),
2030 },
2031 milestones: None,
2032 metadata: spec.metadata.get().clone(),
2033 loop_round: spec.loop_round.as_ref().map(|round| LoopRoundSpec {
2034 max_rounds: round.max_rounds,
2035 min_sleep_ms: round.min_sleep_ms.map(WireU64::get),
2036 max_sleep_ms: round.max_sleep_ms.map(WireU64::get),
2037 default_action: round.default_action.clone(),
2038 }),
2039 exposure_baseline: spec
2040 .exposure_baseline
2041 .as_ref()
2042 .map(|ids| ids.iter().map(|id| id.as_str().into()).collect()),
2043 requested_capabilities: Vec::new(),
2044 requested_budget: None,
2045 }
2046}
2047
2048fn logical_agent_run_spec(spec: &AgentRunSpec) -> LogicalAgentSpec {
2049 LogicalAgentSpec {
2050 goal: spec.goal.clone(),
2051 role: match spec.role {
2052 AgentRole::Custom => None,
2053 AgentRole::Explore => Some(WireRole::Explore),
2054 AgentRole::Plan => Some(WireRole::Plan),
2055 AgentRole::Implement => Some(WireRole::Implement),
2056 AgentRole::Verify => Some(WireRole::Verify),
2057 },
2058 isolation: match spec.isolation {
2059 AgentIsolation::Shared => None,
2060 AgentIsolation::ReadOnly => Some(WireIsolation::ReadOnly),
2061 AgentIsolation::Worktree => Some(WireIsolation::Worktree),
2062 AgentIsolation::Remote => Some(WireIsolation::Remote),
2063 },
2064 context_inheritance: None,
2065 verification_contract_id: spec
2066 .verification_contract_id
2067 .as_ref()
2068 .map(ToString::to_string),
2069 capability_filter: super::root::CapabilityFilter {
2070 allowed_kinds: spec
2071 .capability_filter
2072 .allowed_kinds
2073 .iter()
2074 .copied()
2075 .map(wire_capability_kind)
2076 .collect(),
2077 allowed_ids: spec
2078 .capability_filter
2079 .allowed_ids
2080 .iter()
2081 .map(ToString::to_string)
2082 .collect(),
2083 },
2084 exposure_baseline: spec
2085 .exposure_baseline
2086 .as_ref()
2087 .map(|ids| ids.iter().map(ToString::to_string).collect()),
2088 loop_round: spec
2089 .loop_round
2090 .as_ref()
2091 .map(|round| super::root::LogicalLoopRoundSpec {
2092 max_rounds: round.max_rounds,
2093 min_sleep_ms: round.min_sleep_ms.map(WireU64::new),
2094 max_sleep_ms: round.max_sleep_ms.map(WireU64::new),
2095 default_action: round.default_action.clone(),
2096 }),
2097 metadata: super::scalar::BoundedJson::new(spec.metadata.clone())
2098 .expect("canonical run metadata remains bounded"),
2099 }
2100}
2101
2102fn core_capability_kind(
2103 kind: super::root::CapabilityKind,
2104) -> crate::types::capability::CapabilityKind {
2105 use super::root::CapabilityKind as Wire;
2106 use crate::types::capability::CapabilityKind as Core;
2107 match kind {
2108 Wire::Tool => Core::Tool,
2109 Wire::Skill => Core::Skill,
2110 Wire::Memory => Core::Memory,
2111 Wire::Knowledge => Core::Knowledge,
2112 Wire::McpServer => Core::McpServer,
2113 Wire::Command => Core::Command,
2114 Wire::Agent => Core::Agent,
2115 }
2116}
2117
2118fn wire_capability_kind(
2119 kind: crate::types::capability::CapabilityKind,
2120) -> super::root::CapabilityKind {
2121 use super::root::CapabilityKind as Wire;
2122 use crate::types::capability::CapabilityKind as Core;
2123 match kind {
2124 Core::Tool => Wire::Tool,
2125 Core::Skill => Wire::Skill,
2126 Core::Memory => Wire::Memory,
2127 Core::Knowledge => Wire::Knowledge,
2128 Core::McpServer => Wire::McpServer,
2129 Core::Command => Wire::Command,
2130 Core::Agent => Wire::Agent,
2131 }
2132}
2133
2134fn core_role(role: WireRole) -> AgentRole {
2135 match role {
2136 WireRole::Explore => AgentRole::Explore,
2137 WireRole::Plan => AgentRole::Plan,
2138 WireRole::Implement => AgentRole::Implement,
2139 WireRole::Verify => AgentRole::Verify,
2140 WireRole::Custom => AgentRole::Custom,
2141 }
2142}
2143
2144fn core_isolation(isolation: WireIsolation) -> AgentIsolation {
2145 match isolation {
2146 WireIsolation::Shared => AgentIsolation::Shared,
2147 WireIsolation::ReadOnly => AgentIsolation::ReadOnly,
2148 WireIsolation::Worktree => AgentIsolation::Worktree,
2149 WireIsolation::Remote => AgentIsolation::Remote,
2150 }
2151}
2152
2153fn core_context_inheritance(inheritance: WireContextInheritance) -> ContextInheritance {
2154 match inheritance {
2155 WireContextInheritance::None => ContextInheritance::None,
2156 WireContextInheritance::SystemOnly => ContextInheritance::SystemOnly,
2157 WireContextInheritance::Full => ContextInheritance::Full,
2158 }
2159}
2160
2161fn parse_wire_role(label: &str) -> Option<WireRole> {
2165 match label {
2166 "explore" => Some(WireRole::Explore),
2167 "plan" => Some(WireRole::Plan),
2168 "implement" => Some(WireRole::Implement),
2169 "verify" => Some(WireRole::Verify),
2170 _ => None,
2171 }
2172}
2173
2174fn parse_wire_isolation(label: &str) -> Option<WireIsolation> {
2175 match label {
2176 "read_only" => Some(WireIsolation::ReadOnly),
2177 "worktree" => Some(WireIsolation::Worktree),
2178 "remote" => Some(WireIsolation::Remote),
2179 _ => None,
2180 }
2181}
2182
2183fn parse_wire_context_inheritance(label: &str) -> Option<WireContextInheritance> {
2184 match label {
2185 "none" => Some(WireContextInheritance::None),
2186 "system_only" => Some(WireContextInheritance::SystemOnly),
2187 "full" => Some(WireContextInheritance::Full),
2188 _ => None,
2189 }
2190}
2191
2192fn seed_initial_context(engine: &mut LoopStateMachine, initial: &InitialContext) {
2195 if !initial.messages.is_empty() {
2196 engine.preload_history(initial.messages.iter().map(logical_message).collect());
2197 }
2198 seed_knowledge(engine, &initial.knowledge);
2199 if !initial.requested_capabilities.is_empty() {
2200 engine.set_requested_capabilities(initial.requested_capabilities.clone());
2201 }
2202}
2203
2204fn seed_knowledge(engine: &mut LoopStateMachine, entries: &[super::root::KnowledgeEntry]) {
2210 if entries.is_empty() {
2211 return;
2212 }
2213 let entries: Vec<crate::mm::PageInEntry> = entries
2214 .iter()
2215 .map(|entry| crate::mm::PageInEntry {
2216 content: entry.content.clone(),
2217 tokens: entry.tokens,
2218 source: None,
2219 key: entry.key.clone(),
2220 pinned: entry.pinned,
2221 })
2222 .collect();
2223 engine.apply_page_in(&entries);
2224}
2225
2226fn runtime_signal(
2247 signal: &LogicalSignal,
2248 accepted_at_ms: WireU64,
2249) -> crate::types::signal::RuntimeSignal {
2250 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
2251
2252 let source = match signal.source {
2253 Some(SignalSourceKind::Cron) => SignalSource::Cron,
2254 Some(SignalSourceKind::Gateway) => SignalSource::Gateway,
2255 Some(SignalSourceKind::Heartbeat) => SignalSource::Heartbeat,
2256 Some(SignalSourceKind::Custom) | None => SignalSource::Custom,
2257 };
2258 let urgency = match signal.urgency {
2259 Some(SignalUrgency::Low) => Urgency::Low,
2260 Some(SignalUrgency::High) => Urgency::High,
2261 Some(SignalUrgency::Critical) => Urgency::Critical,
2262 Some(SignalUrgency::Normal) | None => Urgency::Normal,
2263 };
2264 let mut runtime = RuntimeSignal::new(
2265 source,
2266 SignalType::Event,
2271 urgency,
2272 signal_summary(signal),
2273 )
2274 .with_id(signal.signal_id.as_str())
2275 .with_payload(signal.payload.get().clone())
2276 .with_timestamp(accepted_at_ms.get());
2277 if let Some(key) = &signal.dedupe_key {
2278 runtime = runtime.with_dedupe(key.as_str());
2279 }
2280 if let Some(after) = signal.escalate_after_ms {
2285 runtime = runtime.with_deadline(accepted_at_ms.get().saturating_add(after.get()));
2286 }
2287 runtime
2288}
2289
2290fn signal_summary(signal: &LogicalSignal) -> String {
2296 const SIGNAL_SUMMARY_MAX_BYTES: usize = 512;
2297 match signal.payload.get() {
2298 serde_json::Value::Null => signal.signal_id.as_str().to_string(),
2299 serde_json::Value::String(text) => {
2300 truncate_on_char_boundary(text, SIGNAL_SUMMARY_MAX_BYTES)
2301 }
2302 other => truncate_on_char_boundary(&other.to_string(), SIGNAL_SUMMARY_MAX_BYTES),
2303 }
2304}
2305
2306fn live_policy_label(patch: &super::command::LivePolicyPatch) -> &'static str {
2307 use super::command::LivePolicyPatch;
2308 match patch {
2309 LivePolicyPatch::ReplaceSignalPolicy(_) => "signal",
2310 LivePolicyPatch::ReplaceGovernancePolicy(_) => "governance",
2311 LivePolicyPatch::TightenResourceQuota(_) => "resource_quota",
2312 LivePolicyPatch::ReplaceRecoveryPolicy(_) => "recovery",
2313 }
2314}
2315
2316fn logical_message(message: &super::root::LogicalMessage) -> CoreMessage {
2317 CoreMessage {
2318 role: core_role_of(message.role),
2319 content: Content::Text(message.content.clone()),
2320 tool_calls: Vec::new(),
2321 }
2322}
2323
2324fn core_role_of(role: MessageRole) -> Role {
2325 match role {
2326 MessageRole::System => Role::System,
2327 MessageRole::User => Role::User,
2328 MessageRole::Assistant => Role::Assistant,
2329 MessageRole::Tool => Role::Tool,
2330 }
2331}
2332
2333fn wire_role_of(role: Role) -> MessageRole {
2334 match role {
2335 Role::System => MessageRole::System,
2336 Role::User => MessageRole::User,
2337 Role::Assistant => MessageRole::Assistant,
2338 Role::Tool => MessageRole::Tool,
2339 }
2340}
2341
2342fn rendered_context(
2343 context: &crate::context::renderer::InternalRenderedContext,
2344) -> WireRenderedContext {
2345 WireRenderedContext {
2346 system_stable: context.system_stable.clone(),
2347 system_knowledge: context.system_knowledge.clone(),
2348 turns: context.turns.iter().map(provider_message).collect(),
2349 state_turn: context.state_turn.as_ref().map(provider_message),
2350 frozen_prefix_len: context.frozen_prefix_len.map(|len| len as u32),
2351 }
2352}
2353
2354fn provider_message(message: &CoreMessage) -> ProviderMessage {
2355 let (content, tool_call_id) = match &message.content {
2356 Content::Parts(parts) => match parts.as_slice() {
2357 [
2358 ContentPart::ToolResult {
2359 call_id, output, ..
2360 },
2361 ] => (output.clone(), Some(call_id.to_string())),
2362 _ => message_body_parts(message)
2363 .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
2364 .unwrap_or_default(),
2365 },
2366 Content::Text(_) => message_body_parts(message)
2367 .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
2368 .unwrap_or_default(),
2369 };
2370 ProviderMessage {
2371 role: wire_role_of(message.role),
2372 content,
2373 tool_calls: message
2374 .tool_calls
2375 .iter()
2376 .filter_map(|call| wire_tool_call(call).ok())
2377 .collect(),
2378 tool_call_id: tool_call_id.and_then(|call_id| super::scalar::CallId::new(call_id).ok()),
2379 tokens: None,
2380 }
2381}
2382
2383fn tool_schema(schema: &crate::types::message::ToolSchema) -> WireToolSchema {
2384 WireToolSchema {
2385 name: schema.name.to_string(),
2386 description: schema.description.clone(),
2387 parameters: super::scalar::BoundedJson::new(schema.parameters.clone())
2388 .unwrap_or_else(|_| Default::default()),
2389 }
2390}
2391
2392fn workflow_budget(budget: &crate::orchestration::workflow::WorkflowBudget) -> WireWorkflowBudget {
2393 WireWorkflowBudget {
2394 max_total_tokens: budget.tokens_max.map(WireU64::new),
2395 max_turns: None,
2396 max_concurrency: budget.max_concurrent_subagents.map(|max| max as u32),
2397 }
2398}
2399
2400fn sub_agent_result(completed: &ChildCompleted) -> SubAgentResult {
2401 let termination = match completed.result.status {
2402 ChildStatus::Completed => TerminationReason::Completed,
2403 ChildStatus::Failed => TerminationReason::Error,
2404 ChildStatus::Cancelled => TerminationReason::UserAbort,
2405 };
2406 SubAgentResult {
2407 agent_id: completed.task_id.as_str().into(),
2408 result: LoopResult {
2409 termination,
2410 final_message: completed
2411 .result
2412 .output
2413 .as_ref()
2414 .map(|text| CoreMessage::assistant(text.clone())),
2415 turns_used: completed
2416 .result
2417 .usage
2418 .as_ref()
2419 .and_then(|usage| usage.turns)
2420 .unwrap_or(0),
2421 total_tokens_used: completed
2422 .result
2423 .usage
2424 .as_ref()
2425 .and_then(|usage| usage.output_tokens)
2426 .map_or(0, WireU64::get),
2427 loop_continue: None,
2428 classify_branch: None,
2429 pace_decision: None,
2430 tournament_winner: None,
2431 },
2432 }
2433}
2434
2435fn attempt_ordinal(attempt_id: &AttemptId) -> Option<u32> {
2436 attempt_id.as_str().rsplit(':').next()?.parse().ok()
2437}
2438
2439fn supervision_label(policy: crate::scheduler::tcb::ChildFailurePolicy) -> &'static str {
2440 match policy {
2441 crate::scheduler::tcb::ChildFailurePolicy::Propagate => "propagate",
2442 crate::scheduler::tcb::ChildFailurePolicy::Isolate => "isolate",
2443 crate::scheduler::tcb::ChildFailurePolicy::Restart => "restart",
2444 crate::scheduler::tcb::ChildFailurePolicy::Retry => "retry",
2445 crate::scheduler::tcb::ChildFailurePolicy::Ignore => "ignore",
2446 }
2447}
2448
2449fn agent_terminal(result: &LoopResult) -> KernelTerminal {
2456 let usage = UsageReport {
2457 input_tokens: WireU64::new(result.total_tokens_used),
2458 output_tokens: WireU64::ZERO,
2459 turns: result.turns_used,
2460 cached_input_tokens: None,
2461 };
2462 let termination = match result.termination {
2463 TerminationReason::Completed => WireTermination::Completed,
2464 TerminationReason::MaxTurns => WireTermination::MaxTurns,
2465 TerminationReason::TokenBudget => WireTermination::TokenBudget,
2466 TerminationReason::Timeout => WireTermination::Deadline,
2467 TerminationReason::ContextOverflow => WireTermination::ContextOverflow,
2468 TerminationReason::NoProgress => WireTermination::NoProgress,
2469 TerminationReason::MilestoneExceeded => WireTermination::MilestoneExceeded,
2470 TerminationReason::UserAbort => {
2471 return KernelTerminal::Cancelled(CancelledTerminal {
2472 reason: CancellationReason::User,
2473 usage,
2474 });
2475 }
2476 TerminationReason::Error => {
2477 return KernelTerminal::Failed(FailedTerminal {
2478 failure: KernelFailure {
2479 code: KernelFailureCode::InvariantViolated,
2480 message: "the agent loop ended in an error state".to_string(),
2481 },
2482 usage,
2483 });
2484 }
2485 };
2486 KernelTerminal::Agent(AgentTerminal {
2487 result: WireLoopResult {
2488 termination,
2489 final_message: result.final_message.as_ref().map(provider_message),
2490 turns_used: result.turns_used,
2491 pace_decision: result.pace_decision.as_ref().map(|decision| {
2492 super::terminal::PaceDecision {
2493 action: match decision.action {
2494 CorePaceAction::Continue => super::terminal::PaceAction::Continue,
2495 CorePaceAction::Sleep => super::terminal::PaceAction::Sleep,
2496 CorePaceAction::Stop => super::terminal::PaceAction::Stop,
2497 },
2498 delay_ms: decision.delay_ms.map(WireU64::new),
2499 reason: decision.reason.clone(),
2500 coerced_from: decision.coerced_from.clone(),
2501 }
2502 }),
2503 },
2504 usage,
2505 })
2506}
2507
2508fn publishes(disposition: &StepDisposition, tag: EffectKindTag) -> bool {
2509 disposition
2510 .effects()
2511 .iter()
2512 .any(|effect| effect.tag() == tag)
2513}
2514
2515fn loop_action_label(action: &LoopAction) -> &'static str {
2516 match action {
2517 LoopAction::CallLLM { .. } => "call_provider",
2518 LoopAction::ExecuteTools { .. } => "execute_tools",
2519 LoopAction::RequestApproval { .. } => "request_approval",
2520 LoopAction::SpawnWorkflow { .. } => "spawn_tasks",
2521 LoopAction::PreemptSubAgents { .. } => "preempt_tasks",
2522 LoopAction::PersistMemory { .. } => "persist_memory",
2523 LoopAction::QueryMemory { .. } => "query_memory",
2524 LoopAction::ArchivePageOut { .. } => "archive_page_out",
2525 LoopAction::EvaluateMilestone { .. } => "evaluate_milestone",
2526 LoopAction::Done { .. } => "terminal",
2527 LoopAction::AwaitingResume => "awaiting_resume",
2528 }
2529}
2530
2531fn syscall_ack(name: &str) -> &'static str {
2536 match name {
2537 "start_workflow" => {
2538 "workflow accepted: its ready nodes are scheduled; each result arrives as that node \
2539 completes"
2540 }
2541 "submit_workflow_nodes" => {
2542 "nodes appended to the running workflow; each result arrives as that node completes"
2543 }
2544 "skill" => "skill activated: its guidance and tools are in this turn's context",
2545 "update_plan" => "plan updated: the new state renders in [TASK STATE] from here on",
2546 crate::context::manager::MEMORY_TOOL_NAME => {
2547 "memory search issued: matching records are added to this conversation before your \
2548 next turn"
2549 }
2550 crate::context::manager::READ_RESULT_TOOL_NAME => "page-in requested",
2551 "send_message" | "publish_channel" => "local handle routed",
2552 "receive_mailbox" | "receive_channel" | "read_object" => "local state returned",
2553 _ => "accepted",
2554 }
2555}
2556
2557fn validate_ipc_labels(message_id: &str, kind: &str) -> Result<(), SyscallRefusal> {
2558 if message_id.is_empty() || kind.is_empty() || message_id.len() > 256 || kind.len() > 256 {
2559 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2560 "local_ipc",
2561 "message_id and message_kind must contain 1..=256 bytes",
2562 )));
2563 }
2564 Ok(())
2565}
2566
2567fn resolve_ipc_handle(
2568 engine: &LoopStateMachine,
2569 handle_id: &super::scalar::HandleId,
2570) -> Result<crate::mm::handle::Handle, SyscallRefusal> {
2571 engine
2572 .ctx
2573 .handles
2574 .all()
2575 .iter()
2576 .find(|handle| {
2577 handle.source.as_deref() == Some(handle_id.as_str())
2578 || handle.id.to_string() == handle_id.as_str()
2579 })
2580 .cloned()
2581 .ok_or_else(|| {
2582 SyscallRefusal::Rejected(SyscallRejection::new(
2583 "local_ipc",
2584 format!("payload handle {handle_id} is not reachable by this operation"),
2585 ))
2586 })
2587}
2588
2589fn local_ipc_refusal(error: crate::scheduler::tcb::LocalIpcError) -> SyscallRefusal {
2590 let reason = match error {
2591 crate::scheduler::tcb::LocalIpcError::UnknownCaller => "unknown caller",
2592 crate::scheduler::tcb::LocalIpcError::CallerTerminal => "caller is terminal",
2593 crate::scheduler::tcb::LocalIpcError::UnknownRecipient => "unknown recipient",
2594 crate::scheduler::tcb::LocalIpcError::ChannelSubscribersMismatch => {
2595 "channel subscriber set is immutable"
2596 }
2597 crate::scheduler::tcb::LocalIpcError::NotSubscriber => "caller is not a channel subscriber",
2598 crate::scheduler::tcb::LocalIpcError::Full => "IPC capacity is full",
2599 crate::scheduler::tcb::LocalIpcError::Expired => "message TTL already expired",
2600 crate::scheduler::tcb::LocalIpcError::ObjectConflict => {
2601 "object id already names a different descriptor"
2602 }
2603 };
2604 SyscallRefusal::Rejected(SyscallRejection::new("local_ipc", reason))
2605}
2606
2607fn local_ipc_outcome(accepted: bool) -> SyscallOutcome {
2608 SyscallOutcome {
2609 ack: Some(
2610 serde_json::json!({
2611 "status": if accepted { "accepted" } else { "duplicate" },
2612 })
2613 .to_string(),
2614 ),
2615 ..SyscallOutcome::default()
2616 }
2617}
2618
2619fn ipc_messages_outcome(messages: &[crate::scheduler::mailbox::MailboxMessage]) -> SyscallOutcome {
2620 SyscallOutcome {
2621 ack: Some(
2622 serde_json::to_string(messages)
2623 .expect("canonical mailbox messages are always serializable"),
2624 ),
2625 ..SyscallOutcome::default()
2626 }
2627}
2628
2629fn core_provider_message(message: &ProviderMessage) -> Result<CoreMessage, KernelFault> {
2631 Ok(CoreMessage {
2632 role: core_role_of(message.role),
2633 content: Content::Text(message.content.clone()),
2634 tool_calls: message.tool_calls.iter().map(core_tool_call).collect(),
2635 })
2636}
2637
2638fn core_tool_call(call: &WireToolCall) -> crate::types::message::ToolCall {
2639 crate::types::message::ToolCall {
2640 id: call.call_id.as_str().into(),
2641 name: call.name.as_str().into(),
2642 arguments: call.arguments.get().clone(),
2643 }
2644}
2645
2646fn wire_tool_call(call: &crate::types::message::ToolCall) -> Result<WireToolCall, KernelFault> {
2647 Ok(WireToolCall {
2648 call_id: super::scalar::CallId::new(call.id.as_str()).map_err(malformed)?,
2649 name: call.name.to_string(),
2650 arguments: super::scalar::BoundedJson::new(call.arguments.clone())
2651 .unwrap_or_else(|_| Default::default()),
2652 })
2653}
2654
2655fn wire_approval_request(
2656 request: &crate::scheduler::state_machine::ApprovalRequest,
2657) -> Result<WireApprovalRequest, KernelFault> {
2658 Ok(WireApprovalRequest {
2659 call_id: super::scalar::CallId::new(request.call_id.as_str()).map_err(malformed)?,
2660 tool_name: request.tool.clone(),
2661 arguments: super::scalar::BoundedJson::new(request.arguments.clone())
2662 .unwrap_or_else(|_| Default::default()),
2663 reason: (!request.reason.is_empty()).then(|| request.reason.clone()),
2664 })
2665}
2666
2667fn core_tool_result(payload: &WireToolResultPayload) -> ToolResult {
2689 let disposition = payload.disposition();
2690 let is_error = payload.is_error();
2691 let error_kind = match disposition {
2692 ToolResultDisposition::Fatal => Some(ToolErrorKind::Fatal),
2693 ToolResultDisposition::Recoverable => is_error.then_some(ToolErrorKind::Recoverable),
2694 };
2695 match payload {
2696 WireToolResultPayload::Inline(inline) => ToolResult {
2697 call_id: inline.call_id.as_str().into(),
2698 output: Content::Text(inline.result.output.clone()),
2699 durable_content: inline.result.durable_content.clone(),
2700 is_error,
2701 is_fatal: disposition.is_fatal(),
2702 error_kind,
2703 },
2704 WireToolResultPayload::External(external) => ToolResult {
2705 call_id: external.call_id.as_str().into(),
2706 output: Content::Text(external.preview.clone()),
2707 durable_content: None,
2708 is_error,
2709 is_fatal: disposition.is_fatal(),
2710 error_kind,
2711 },
2712 }
2713}
2714
2715fn check_payload_policy(
2731 payload: &WireToolResultPayload,
2732 policy: &super::config::ResolvedPayloadPolicy,
2733) -> Result<(), KernelFault> {
2734 let threshold = policy.inline_threshold_bytes as u64;
2735 match payload {
2736 WireToolResultPayload::Inline(inline) => {
2737 let durable_size = inline
2738 .result
2739 .durable_content
2740 .as_ref()
2741 .map(|content| {
2742 content.validate().map_err(|error| {
2743 KernelFault::new(
2744 KernelFaultCode::MalformedEnvelope,
2745 format!(
2746 "inline tool result {} carries invalid durable content: {error}",
2747 inline.call_id
2748 ),
2749 )
2750 })?;
2751 serde_json::to_vec(content).map(|bytes| bytes.len() as u64).map_err(|error| {
2752 KernelFault::new(
2753 KernelFaultCode::MalformedEnvelope,
2754 format!(
2755 "inline tool result {} durable content cannot be encoded: {error}",
2756 inline.call_id
2757 ),
2758 )
2759 })
2760 })
2761 .transpose()?
2762 .unwrap_or(0);
2763 let size = (inline.result.output.len() as u64).max(durable_size);
2764 if size >= threshold {
2765 return Err(KernelFault::new(
2766 KernelFaultCode::ResourceLimitExceeded,
2767 format!(
2768 "tool result {} is {size} bytes and this operation's payload policy \
2769 externalises at {threshold}; the host persists the body and submits an \
2770 external result — the kernel does not spool on its behalf (§7.10)",
2771 inline.call_id
2772 ),
2773 ));
2774 }
2775 Ok(())
2776 }
2777 WireToolResultPayload::External(external) => {
2778 if !is_verifiable_digest(external.digest.as_str()) {
2779 return Err(KernelFault::new(
2780 KernelFaultCode::MalformedEnvelope,
2781 format!(
2782 "external tool result {} carries digest {}, which this kernel cannot \
2783 verify; a paged-in body is checked by recomputing {}:<64 hex> over it",
2784 external.call_id,
2785 external.digest,
2786 super::record::DIGEST_ALGORITHM
2787 ),
2788 ));
2789 }
2790 let size = external.original_size.get();
2791 if size < threshold {
2792 return Err(KernelFault::new(
2793 KernelFaultCode::MalformedEnvelope,
2794 format!(
2795 "external tool result {} declares {size} bytes but this operation's \
2796 payload policy inlines below {threshold}; the threshold is the single \
2797 arbiter of which arm a result takes (§7.10)",
2798 external.call_id
2799 ),
2800 ));
2801 }
2802 let preview = external.preview.len() as u64;
2803 if preview > policy.preview_bytes as u64 {
2804 return Err(KernelFault::new(
2805 KernelFaultCode::ResourceLimitExceeded,
2806 format!(
2807 "external tool result {} carries a {preview}-byte preview and this \
2808 operation keeps {} bytes resident",
2809 external.call_id, policy.preview_bytes
2810 ),
2811 ));
2812 }
2813 Ok(())
2814 }
2815 }
2816}
2817
2818fn is_verifiable_digest(digest: &str) -> bool {
2820 let Some(hex) = digest.strip_prefix(super::record::DIGEST_ALGORITHM) else {
2821 return false;
2822 };
2823 let Some(hex) = hex.strip_prefix(':') else {
2824 return false;
2825 };
2826 hex.len() == 64
2827 && hex
2828 .bytes()
2829 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
2830}
2831
2832fn core_milestone_result(
2833 result: &super::effect::MilestoneCheckResult,
2834) -> crate::types::milestone::MilestoneCheckResult {
2835 crate::types::milestone::MilestoneCheckResult {
2836 phase_id: result.phase_id.clone(),
2837 passed: result.passed,
2838 reason: (!result.passed).then(|| {
2839 if result.failed_criteria.is_empty() {
2840 result.notes.clone()
2841 } else {
2842 format!("unmet criteria: {}", result.failed_criteria.join("; "))
2843 }
2844 }),
2845 }
2846}
2847
2848fn core_milestone_contract(
2857 contract: &super::config::VerificationContract,
2858 config: &ResolvedOperationConfig,
2859) -> crate::types::milestone::MilestoneContract {
2860 use crate::types::capability::{CapabilityDescriptor, CapabilityKind as CoreCapabilityKind};
2861 use crate::types::milestone::{MilestoneContract, MilestonePhase};
2862
2863 let mut cascade = MilestoneContract::new();
2864 for phase in &contract.phases {
2865 let unlocks = phase
2866 .unlocks
2867 .iter()
2868 .map(|id| {
2869 if let Some(tool) = config.tool_catalog.iter().find(|tool| &tool.name == id) {
2870 CapabilityDescriptor::tool(core_tool_schema(tool))
2871 } else if let Some(skill) = config.skill_catalog.iter().find(|s| &s.name == id) {
2872 CapabilityDescriptor::skill(core_skill(skill))
2873 } else {
2874 CapabilityDescriptor::marker(
2875 CoreCapabilityKind::Tool,
2876 id.as_str(),
2877 String::new(),
2878 )
2879 }
2880 })
2881 .collect();
2882 cascade = cascade.phase(MilestonePhase {
2883 unlocks,
2884 ..MilestonePhase::new(phase.phase_id.clone())
2885 });
2886 }
2887 cascade
2888}
2889
2890fn core_memory_kind(kind: WireMemoryKind) -> crate::mm::memory::MemoryKind {
2891 match kind {
2892 WireMemoryKind::User => crate::mm::memory::MemoryKind::User,
2893 WireMemoryKind::Feedback => crate::mm::memory::MemoryKind::Feedback,
2894 WireMemoryKind::Project => crate::mm::memory::MemoryKind::Project,
2895 WireMemoryKind::Reference => crate::mm::memory::MemoryKind::Reference,
2896 }
2897}
2898
2899fn wire_memory_kind_label(kind: WireMemoryKind) -> &'static str {
2900 core_memory_kind(kind).label()
2901}
2902
2903fn binding_scope(binding_id: &MemoryBindingId) -> crate::mm::memory::MemoryScope {
2908 crate::mm::memory::MemoryScope::new(String::new(), binding_id.as_str().to_string())
2909}
2910
2911fn host_failure_text(failure: &HostEffectFailure) -> String {
2914 if failure.message.is_empty() {
2915 failure.kind.as_str().to_string()
2916 } else {
2917 format!("{}: {}", failure.kind.as_str(), failure.message)
2918 }
2919}
2920
2921fn unowned_resolution(effect_id: &EffectId, what: &str) -> KernelFault {
2925 KernelFault::new(
2926 KernelFaultCode::RecordCorrupted,
2927 format!(
2928 "effect {effect_id} resolves a {what} this runtime never authored; the driver's ledger \
2929 no longer describes the journal — rebuild from the records"
2930 ),
2931 )
2932}
2933
2934fn truncate_on_char_boundary(text: &str, max_bytes: usize) -> String {
2935 if text.len() <= max_bytes {
2936 return text.to_string();
2937 }
2938 let mut end = max_bytes;
2939 while end > 0 && !text.is_char_boundary(end) {
2940 end -= 1;
2941 }
2942 text[..end].to_string()
2943}
2944
2945fn build_engine(config: &ResolvedOperationConfig) -> LoopStateMachine {
2953 let execution = &config.execution_policy;
2954 let mut engine = LoopStateMachine::new(SchedulerBudget {
2955 max_tokens: execution.max_context_tokens,
2956 max_turns: execution.max_turns,
2957 max_total_tokens: execution.max_total_tokens.get(),
2958 max_wall_ms: execution.max_wall_ms.map(WireU64::get),
2959 });
2960 if let Some(grant) = config.budget_grant.clone() {
2961 engine.set_budget_grant(grant);
2962 }
2963 let scheduler_policy = config.scheduler_policy;
2964 engine.set_scheduler_policy(crate::scheduler::policy::SchedulerPolicyConfig {
2965 critical_path_weight: i64::from(scheduler_policy.critical_path_weight),
2966 fanout_weight: i64::from(scheduler_policy.fanout_weight),
2967 age_weight: i64::from(scheduler_policy.age_weight),
2968 token_cost_weight: i64::from(scheduler_policy.token_cost_weight),
2969 deadline_weight: i64::from(scheduler_policy.deadline_weight),
2970 process_priority_weight: i64::from(scheduler_policy.process_priority_weight),
2971 resource_pressure_weight: i64::from(scheduler_policy.resource_pressure_weight),
2972 budget_pressure_weight: i64::from(scheduler_policy.budget_pressure_weight),
2973 });
2974
2975 engine.set_criteria_gate(execution.criteria_gate_enabled);
2976 engine.set_repeat_fuse(crate::governance::repeat_fuse::RepeatFuseConfig {
2977 enabled: execution.repeat_fuse.enabled,
2978 deny_after: execution.repeat_fuse.deny_after,
2979 terminate_after: execution.repeat_fuse.terminate_after,
2980 });
2981 engine.set_entropy_watch(crate::scheduler::entropy::EntropyWatchConfig {
2982 enabled: execution.entropy_watch.enabled,
2983 threshold: f64::from(execution.entropy_watch.threshold_ppm.get()) / 1_000_000.0,
2984 hysteresis: f64::from(execution.entropy_watch.hysteresis_ppm.get()) / 1_000_000.0,
2985 cooldown_turns: execution.entropy_watch.cooldown_turns,
2986 notify_model: execution.entropy_watch.notify_model,
2987 });
2988 install_live_policies(&mut engine, config);
2989 engine
2990 .ctx
2991 .set_memory_enabled(config.feature_policy.memory_enabled);
2992 engine
2993 .ctx
2994 .set_knowledge_enabled(config.feature_policy.knowledge_enabled);
2995 engine
2996 .ctx
2997 .set_plan_tool_enabled(config.feature_policy.plan_tool_enabled);
2998 engine
3001 .ctx
3002 .set_available_skills(config.skill_catalog.iter().map(core_skill).collect());
3003 engine.ctx.set_stable_core_tools(
3004 config
3005 .feature_policy
3006 .stable_core_tool_ids
3007 .iter()
3008 .map(|id| id.as_str().into()),
3009 );
3010 engine.ctx.config.knowledge_budget_ratio =
3011 config.context_policy.knowledge_budget_ppm.as_ratio();
3012 engine.ctx.config.collapse_assistant_narration =
3013 config.context_policy.collapse_old_assistant_narration;
3014 engine.tools = config.tool_catalog.iter().map(core_tool_schema).collect();
3015 engine
3016}
3017
3018fn install_live_policies(engine: &mut LoopStateMachine, config: &ResolvedOperationConfig) {
3027 if let Some(quota) = core_quota(&config.resource_quota) {
3031 engine.set_resource_quota(quota);
3032 }
3033 if let Some(pipeline) = core_governance(&config.governance_policy) {
3037 engine.set_governance(pipeline);
3038 }
3039 engine.set_signal_policy(
3040 config.signal_policy.queue_max as usize,
3041 config.signal_policy.ttl_ms.map(WireU64::get),
3042 config.signal_policy.deadline_escalation,
3043 );
3044 engine.set_recovery_limits(
3050 config.recovery_policy.provider_recovery_attempts,
3051 config.recovery_policy.output_recovery_attempts,
3052 );
3053}
3054
3055fn core_quota(
3059 quota: &super::config::ResourceQuota,
3060) -> Option<crate::governance::quota::ResourceQuota> {
3061 if quota == &super::config::ResourceQuota::default() {
3062 return None;
3063 }
3064 Some(crate::governance::quota::ResourceQuota {
3065 max_concurrent_subagents: quota.max_concurrent_subagents,
3066 max_total_subagents: quota.max_total_subagents,
3067 max_spawn_depth: quota.max_spawn_depth,
3068 memory_writes_per_window: quota
3069 .memory_writes_per_window
3070 .as_ref()
3071 .map(|window| (window.max_events, window.window_ms.get())),
3072 max_workflow_nodes: quota.max_workflow_nodes.map(|max| max as usize),
3073 })
3074}
3075
3076fn core_governance(
3080 policy: &super::config::ResolvedGovernancePolicy,
3081) -> Option<crate::governance::pipeline::GovernancePipeline> {
3082 use super::command::{ParamConstraint as WireConstraint, PolicyAction};
3083 use crate::governance::constraint::{ConstraintRule, ParamConstraint as CoreConstraint};
3084 use crate::governance::permission::PermissionRule;
3085 use crate::governance::rate_limit::RateLimit;
3086
3087 if policy.default_action == PolicyAction::Allow
3088 && policy.rules.is_empty()
3089 && policy.vetoed_tools.is_empty()
3090 && policy.rate_limits.is_empty()
3091 && policy.constraints.is_empty()
3092 {
3093 return None;
3094 }
3095 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(core_policy_action(
3096 policy.default_action,
3097 ));
3098 for rule in &policy.rules {
3099 pipeline.permission.add_rule(PermissionRule {
3100 tool_pattern: rule.tool_pattern.as_str().into(),
3101 action: core_policy_action(rule.action),
3102 });
3103 }
3104 for tool in &policy.vetoed_tools {
3105 pipeline.veto.block_tool(tool.clone());
3106 }
3107 for limit in &policy.rate_limits {
3108 pipeline.rate_limiter.set_limit(
3109 limit.tool.clone(),
3110 RateLimit {
3111 max_calls: limit.max_calls,
3112 window_ms: limit.window_ms.get(),
3113 },
3114 );
3115 }
3116 for constraint in &policy.constraints {
3117 let rule = match constraint {
3118 WireConstraint::Required(_) => ConstraintRule::Required,
3119 WireConstraint::Enum(spec) => ConstraintRule::Enum(spec.values.clone()),
3120 WireConstraint::Range(spec) => ConstraintRule::Range {
3123 min: spec.min_micros.map(|micros| micros as f64 / 1_000_000.0),
3124 max: spec.max_micros.map(|micros| micros as f64 / 1_000_000.0),
3125 },
3126 };
3127 pipeline.constraints.add(CoreConstraint {
3128 tool_name: constraint.tool().to_string(),
3129 param_path: constraint.param_path().to_string(),
3130 rule,
3131 });
3132 }
3133 Some(pipeline)
3134}
3135
3136fn core_policy_action(
3137 action: super::command::PolicyAction,
3138) -> crate::governance::permission::PermissionAction {
3139 use crate::governance::permission::PermissionAction;
3140 match action {
3141 super::command::PolicyAction::Allow => PermissionAction::Allow,
3142 super::command::PolicyAction::Deny => PermissionAction::Deny,
3143 super::command::PolicyAction::AskUser => PermissionAction::AskUser,
3144 }
3145}
3146
3147fn core_skill(skill: &super::config::SkillMetadata) -> crate::types::skill::SkillMetadata {
3148 crate::types::skill::SkillMetadata {
3149 name: skill.name.as_str().into(),
3150 description: skill.description.clone(),
3151 when_to_use: skill.when_to_use.clone(),
3152 allowed_tools: skill
3153 .allowed_tools
3154 .iter()
3155 .map(|tool| tool.as_str().into())
3156 .collect(),
3157 capability_grants: skill.capability_grants.clone(),
3158 effort: skill.effort,
3159 estimated_tokens: skill.estimated_tokens.unwrap_or(0),
3160 }
3161}
3162
3163fn ensure_skill_grants_are_attenuated(
3164 grants: &[crate::types::capability::Capability],
3165 parent_capabilities: &[crate::types::capability::Capability],
3166) -> Result<(), Vec<crate::types::capability::Capability>> {
3167 crate::types::capability::caps_subset(grants, parent_capabilities)
3168}
3169
3170fn skill_grant_attenuation_message(
3171 skill_name: &str,
3172 violations: &[crate::types::capability::Capability],
3173) -> String {
3174 format!(
3175 "skill {skill_name:?} declares capability grants that would widen the mounting agent's authority: {}",
3176 violations
3177 .iter()
3178 .map(|capability| capability.id.0.as_str())
3179 .collect::<Vec<_>>()
3180 .join(", ")
3181 )
3182}
3183
3184fn core_tool_schema(schema: &WireToolSchema) -> crate::types::message::ToolSchema {
3185 crate::types::message::ToolSchema {
3186 name: schema.name.as_str().into(),
3187 description: schema.description.clone(),
3188 parameters: schema.parameters.get().clone(),
3189 }
3190}
3191
3192fn malformed(error: super::scalar::WireScalarError) -> KernelFault {
3193 KernelFault::new(KernelFaultCode::MalformedEnvelope, error.message)
3194}
3195
3196#[cfg(test)]
3197mod tests;