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