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] = &[
1694 "start_workflow",
1695 "submit_workflow_nodes",
1696 "skill",
1697 "update_plan",
1698 crate::context::manager::MEMORY_TOOL_NAME,
1699 crate::context::manager::READ_RESULT_TOOL_NAME,
1700 "send_message",
1701 "publish_channel",
1702 "receive_mailbox",
1703 "receive_channel",
1704 "read_object",
1705];
1706
1707fn is_syscall_tool(name: &str) -> bool {
1708 SYSCALL_TOOL_NAMES.contains(&name)
1709}
1710
1711fn decode_syscall(call: &WireToolCall) -> Result<SyscallRequest, SyscallRejection> {
1716 let arguments = call.arguments.get().clone();
1717 let name: &'static str = SYSCALL_TOOL_NAMES
1718 .iter()
1719 .copied()
1720 .find(|known| *known == call.name.as_str())
1721 .expect("only recognised syscall tools reach the decoder");
1722
1723 fn decode<T: serde::de::DeserializeOwned>(
1724 name: &'static str,
1725 arguments: serde_json::Value,
1726 ) -> Result<T, SyscallRejection> {
1727 serde_json::from_value(arguments)
1728 .map_err(|error| SyscallRejection::new(name, format!("malformed arguments: {error}")))
1729 }
1730
1731 match name {
1732 "start_workflow" => Ok(SyscallRequest::SubmitWorkflow(
1733 super::syscall::SubmitWorkflowRequest {
1734 spec: decode(name, arguments)?,
1735 },
1736 )),
1737 "submit_workflow_nodes" => {
1738 #[derive(serde::Deserialize)]
1739 struct Args {
1740 nodes: Vec<WireNode>,
1741 }
1742 let args: Args = decode(name, arguments)?;
1743 Ok(SyscallRequest::AppendWorkflowNodes(
1744 super::syscall::AppendWorkflowNodesRequest { nodes: args.nodes },
1745 ))
1746 }
1747 "skill" => {
1748 #[derive(serde::Deserialize)]
1749 struct Args {
1750 name: String,
1751 #[serde(default)]
1752 lease_turns: Option<u32>,
1753 }
1754 let args: Args = decode(name, arguments)?;
1755 Ok(SyscallRequest::ActivateSkill(
1756 super::syscall::ActivateSkillRequest {
1757 name: args.name,
1758 lease_turns: args.lease_turns,
1759 },
1760 ))
1761 }
1762 "update_plan" => Ok(SyscallRequest::UpdateTask(
1763 super::syscall::UpdateTaskRequest {
1764 update: decode(name, arguments)?,
1765 },
1766 )),
1767 crate::context::manager::MEMORY_TOOL_NAME => {
1768 #[derive(serde::Deserialize)]
1769 struct Args {
1770 #[serde(default)]
1771 query: String,
1772 #[serde(default)]
1773 kinds: Vec<WireMemoryKind>,
1774 #[serde(default)]
1775 top_k: Option<u32>,
1776 }
1777 let args: Args = decode(name, arguments)?;
1778 Ok(SyscallRequest::RequestMemoryQuery(
1779 super::syscall::RequestMemoryQueryRequest {
1780 query: super::syscall::MemoryQueryProposal {
1781 text: args.query,
1782 kinds: args.kinds,
1783 limit: args.top_k,
1784 },
1785 },
1786 ))
1787 }
1788 crate::context::manager::READ_RESULT_TOOL_NAME => {
1789 #[derive(serde::Deserialize)]
1790 struct Args {
1791 call_id: String,
1792 }
1793 let args: Args = decode(name, arguments)?;
1794 let handle_id = super::scalar::HandleId::new(args.call_id).map_err(|error| {
1795 SyscallRejection::new(name, format!("malformed handle: {}", error.message))
1796 })?;
1797 Ok(SyscallRequest::PageIn(super::syscall::PageInRequest {
1798 handle_id,
1799 }))
1800 }
1801 "send_message" => Ok(SyscallRequest::SendMessage(decode(name, arguments)?)),
1802 "publish_channel" => Ok(SyscallRequest::PublishChannel(decode(name, arguments)?)),
1803 "receive_mailbox" => Ok(SyscallRequest::ReceiveMailbox(decode(name, arguments)?)),
1804 "receive_channel" => Ok(SyscallRequest::ReceiveChannel(decode(name, arguments)?)),
1805 "read_object" => Ok(SyscallRequest::ReadObject(decode(name, arguments)?)),
1806 other => unreachable!("unrecognised syscall tool {other}"),
1807 }
1808}
1809
1810fn causation_task(causation: &SyscallCausation) -> TaskId {
1812 match causation {
1813 SyscallCausation::ProviderTool(provider) => provider.task_id.clone(),
1814 SyscallCausation::ChildAttempt(child) => child.task_id.clone(),
1815 }
1816}
1817
1818fn privileged_family(request: &SyscallRequest) -> Option<&'static str> {
1828 match request {
1829 SyscallRequest::SubmitWorkflow(_) | SyscallRequest::AppendWorkflowNodes(_) => {
1830 Some("workflow")
1831 }
1832 SyscallRequest::RequestMemoryWrite(_) | SyscallRequest::RequestMemoryQuery(_) => {
1833 Some("memory")
1834 }
1835 SyscallRequest::ActivateSkill(_) => Some("capability"),
1836 SyscallRequest::SendMessage(_) | SyscallRequest::PublishChannel(_) => Some("ipc"),
1837 SyscallRequest::UpdateTask(_)
1838 | SyscallRequest::PageIn(_)
1839 | SyscallRequest::ReceiveMailbox(_)
1840 | SyscallRequest::ReceiveChannel(_)
1841 | SyscallRequest::ReadObject(_) => None,
1842 }
1843}
1844
1845fn core_task_update(update: &WireTaskUpdate) -> crate::context::task_state::TaskUpdate {
1846 crate::context::task_state::TaskUpdate {
1847 plan: update.plan.clone(),
1848 current_step: update.current_step.map(|step| step as usize),
1849 progress: update.progress.clone(),
1850 scratchpad: update.scratchpad.clone(),
1851 blocked_on: update.blocked_on.clone(),
1852 preserved_refs: update.preserved_refs.clone(),
1853 directives: update.directives.clone(),
1854 }
1855}
1856
1857fn mint_effect_id(operation_id: &OperationId, step_seq: WireU64, index: u32) -> EffectId {
1858 EffectId::new(format!("{operation_id}:step:{step_seq}:effect:{index}"))
1859 .expect("an operation-scoped effect id is always a legal branded ref")
1860}
1861
1862fn mint_workflow_id(operation_id: &OperationId, step_seq: WireU64) -> WorkflowId {
1863 WorkflowId::new(format!("{operation_id}:workflow:{step_seq}"))
1864 .expect("an operation-scoped workflow id is always a legal branded ref")
1865}
1866
1867fn parse_node_index(agent_id: &str) -> Option<usize> {
1869 let rest = agent_id.strip_prefix("wf-node")?;
1870 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
1871 digits.parse().ok()
1872}
1873
1874fn wire_node_ids(spec: &WireSpec) -> Vec<NodeId> {
1875 spec.nodes.iter().map(|node| node.node_id.clone()).collect()
1876}
1877
1878fn build_core_spec(spec: &WireSpec) -> Result<CoreWorkflowSpec, KernelFault> {
1881 let mut index_of: BTreeMap<&str, usize> = BTreeMap::new();
1882 for (index, node) in spec.nodes.iter().enumerate() {
1883 if index_of.insert(node.node_id.as_str(), index).is_some() {
1884 return Err(KernelFault::new(
1885 KernelFaultCode::InvalidConfig,
1886 format!(
1887 "workflow node id {:?} appears twice; node identity is unique within a DAG",
1888 node.node_id
1889 ),
1890 ));
1891 }
1892 }
1893 let mut nodes = Vec::with_capacity(spec.nodes.len());
1894 for node in &spec.nodes {
1895 let role = node
1896 .run_spec
1897 .as_ref()
1898 .and_then(|spec| spec.role)
1899 .map_or(AgentRole::Custom, core_role);
1900 let mut core = CoreWorkflowNode::new(runtime_task(&node.task), role);
1901 if let Some(isolation) = node.run_spec.as_ref().and_then(|spec| spec.isolation) {
1902 core = core.with_isolation(core_isolation(isolation));
1903 }
1904 if let Some(inheritance) = node
1905 .run_spec
1906 .as_ref()
1907 .and_then(|spec| spec.context_inheritance)
1908 {
1909 core.context_inheritance = core_context_inheritance(inheritance);
1910 }
1911 if let Some(metadata) = node
1912 .run_spec
1913 .as_ref()
1914 .map(|spec| spec.metadata.get())
1915 .and_then(serde_json::Value::as_object)
1916 {
1917 if let Some(model_hint) = metadata
1918 .get("model_hint")
1919 .and_then(serde_json::Value::as_str)
1920 {
1921 core = core.with_model_hint(model_hint);
1922 }
1923 if let Some(output_schema) = metadata.get("output_schema") {
1924 core = core.with_output_schema(output_schema.clone());
1925 }
1926 if let Some(requested) = metadata.get("requested_capabilities") {
1932 let capabilities: Vec<crate::types::capability::Capability> =
1933 serde_json::from_value(requested.clone()).map_err(|error| {
1934 KernelFault::new(
1935 KernelFaultCode::InvalidConfig,
1936 format!(
1937 "workflow node {:?} metadata.requested_capabilities is malformed: {error}",
1938 node.node_id
1939 ),
1940 )
1941 })?;
1942 core = core.with_requested_capabilities(capabilities);
1943 }
1944 if let Some(requested) = metadata.get("requested_budget") {
1947 let budget: crate::scheduler::budget_grant::ResourceBudget =
1948 serde_json::from_value(requested.clone()).map_err(|error| {
1949 KernelFault::new(
1950 KernelFaultCode::InvalidConfig,
1951 format!(
1952 "workflow node {:?} metadata.requested_budget is malformed: {error}",
1953 node.node_id
1954 ),
1955 )
1956 })?;
1957 core = core.with_requested_budget(budget);
1958 }
1959 if let Some(factors) = metadata.get("scheduling_factors") {
1960 let factors: crate::orchestration::task_graph::SchedulingFactors =
1961 serde_json::from_value(factors.clone()).map_err(|error| {
1962 KernelFault::new(
1963 KernelFaultCode::InvalidConfig,
1964 format!(
1965 "workflow node {:?} metadata.scheduling_factors is malformed: {error}",
1966 node.node_id
1967 ),
1968 )
1969 })?;
1970 core = core.with_scheduling_factors(factors);
1971 }
1972 }
1973 let mut depends_on = Vec::with_capacity(node.depends_on.len());
1974 for dependency in &node.depends_on {
1975 let Some(&index) = index_of.get(dependency.as_str()) else {
1976 return Err(KernelFault::new(
1977 KernelFaultCode::InvalidConfig,
1978 format!(
1979 "workflow node {:?} depends on {:?}, which this DAG does not declare",
1980 node.node_id, dependency
1981 ),
1982 ));
1983 };
1984 depends_on.push(index);
1985 }
1986 nodes.push(core.with_depends_on(depends_on));
1987 }
1988 let core = CoreWorkflowSpec::new(nodes);
1989 core.validate()
1990 .map_err(|error| KernelFault::new(KernelFaultCode::InvalidConfig, error.to_string()))?;
1991 Ok(core)
1992}
1993
1994fn runtime_task(task: &LogicalTask) -> RuntimeTask {
1995 RuntimeTask {
1996 goal: task.goal.clone(),
1997 criteria: task.criteria.clone(),
1998 metadata: task.metadata.get().clone(),
1999 lane: task.lane.as_ref().map(TaskLane::new).unwrap_or_default(),
2000 }
2001}
2002
2003fn agent_run_spec(spec: &LogicalAgentSpec) -> AgentRunSpec {
2005 AgentRunSpec {
2006 identity: AgentIdentity::new(ROOT_TASK_ID, NO_HOST_SESSION),
2007 role: spec.role.map_or(AgentRole::Custom, core_role),
2008 isolation: spec
2009 .isolation
2010 .map_or(AgentIsolation::Shared, core_isolation),
2011 goal: spec.goal.clone(),
2012 verification_contract_id: spec.verification_contract_id.as_deref().map(Into::into),
2013 capability_filter: AgentCapabilityFilter {
2014 allowed_kinds: spec
2015 .capability_filter
2016 .allowed_kinds
2017 .iter()
2018 .copied()
2019 .map(core_capability_kind)
2020 .collect(),
2021 allowed_ids: spec
2022 .capability_filter
2023 .allowed_ids
2024 .iter()
2025 .map(|id| id.as_str().into())
2026 .collect(),
2027 },
2028 milestones: None,
2029 metadata: spec.metadata.get().clone(),
2030 loop_round: spec.loop_round.as_ref().map(|round| LoopRoundSpec {
2031 max_rounds: round.max_rounds,
2032 min_sleep_ms: round.min_sleep_ms.map(WireU64::get),
2033 max_sleep_ms: round.max_sleep_ms.map(WireU64::get),
2034 default_action: round.default_action.clone(),
2035 }),
2036 exposure_baseline: spec
2037 .exposure_baseline
2038 .as_ref()
2039 .map(|ids| ids.iter().map(|id| id.as_str().into()).collect()),
2040 requested_capabilities: Vec::new(),
2041 requested_budget: None,
2042 }
2043}
2044
2045fn logical_agent_run_spec(spec: &AgentRunSpec) -> LogicalAgentSpec {
2046 LogicalAgentSpec {
2047 goal: spec.goal.clone(),
2048 role: match spec.role {
2049 AgentRole::Custom => None,
2050 AgentRole::Explore => Some(WireRole::Explore),
2051 AgentRole::Plan => Some(WireRole::Plan),
2052 AgentRole::Implement => Some(WireRole::Implement),
2053 AgentRole::Verify => Some(WireRole::Verify),
2054 },
2055 isolation: match spec.isolation {
2056 AgentIsolation::Shared => None,
2057 AgentIsolation::ReadOnly => Some(WireIsolation::ReadOnly),
2058 AgentIsolation::Worktree => Some(WireIsolation::Worktree),
2059 AgentIsolation::Remote => Some(WireIsolation::Remote),
2060 },
2061 context_inheritance: None,
2062 verification_contract_id: spec
2063 .verification_contract_id
2064 .as_ref()
2065 .map(ToString::to_string),
2066 capability_filter: super::root::CapabilityFilter {
2067 allowed_kinds: spec
2068 .capability_filter
2069 .allowed_kinds
2070 .iter()
2071 .copied()
2072 .map(wire_capability_kind)
2073 .collect(),
2074 allowed_ids: spec
2075 .capability_filter
2076 .allowed_ids
2077 .iter()
2078 .map(ToString::to_string)
2079 .collect(),
2080 },
2081 exposure_baseline: spec
2082 .exposure_baseline
2083 .as_ref()
2084 .map(|ids| ids.iter().map(ToString::to_string).collect()),
2085 loop_round: spec
2086 .loop_round
2087 .as_ref()
2088 .map(|round| super::root::LogicalLoopRoundSpec {
2089 max_rounds: round.max_rounds,
2090 min_sleep_ms: round.min_sleep_ms.map(WireU64::new),
2091 max_sleep_ms: round.max_sleep_ms.map(WireU64::new),
2092 default_action: round.default_action.clone(),
2093 }),
2094 metadata: super::scalar::BoundedJson::new(spec.metadata.clone())
2095 .expect("canonical run metadata remains bounded"),
2096 }
2097}
2098
2099fn core_capability_kind(
2100 kind: super::root::CapabilityKind,
2101) -> crate::types::capability::CapabilityKind {
2102 use super::root::CapabilityKind as Wire;
2103 use crate::types::capability::CapabilityKind as Core;
2104 match kind {
2105 Wire::Tool => Core::Tool,
2106 Wire::Skill => Core::Skill,
2107 Wire::Memory => Core::Memory,
2108 Wire::Knowledge => Core::Knowledge,
2109 Wire::McpServer => Core::McpServer,
2110 Wire::Command => Core::Command,
2111 Wire::Agent => Core::Agent,
2112 }
2113}
2114
2115fn wire_capability_kind(
2116 kind: crate::types::capability::CapabilityKind,
2117) -> super::root::CapabilityKind {
2118 use super::root::CapabilityKind as Wire;
2119 use crate::types::capability::CapabilityKind as Core;
2120 match kind {
2121 Core::Tool => Wire::Tool,
2122 Core::Skill => Wire::Skill,
2123 Core::Memory => Wire::Memory,
2124 Core::Knowledge => Wire::Knowledge,
2125 Core::McpServer => Wire::McpServer,
2126 Core::Command => Wire::Command,
2127 Core::Agent => Wire::Agent,
2128 }
2129}
2130
2131fn core_role(role: WireRole) -> AgentRole {
2132 match role {
2133 WireRole::Explore => AgentRole::Explore,
2134 WireRole::Plan => AgentRole::Plan,
2135 WireRole::Implement => AgentRole::Implement,
2136 WireRole::Verify => AgentRole::Verify,
2137 WireRole::Custom => AgentRole::Custom,
2138 }
2139}
2140
2141fn core_isolation(isolation: WireIsolation) -> AgentIsolation {
2142 match isolation {
2143 WireIsolation::Shared => AgentIsolation::Shared,
2144 WireIsolation::ReadOnly => AgentIsolation::ReadOnly,
2145 WireIsolation::Worktree => AgentIsolation::Worktree,
2146 WireIsolation::Remote => AgentIsolation::Remote,
2147 }
2148}
2149
2150fn core_context_inheritance(inheritance: WireContextInheritance) -> ContextInheritance {
2151 match inheritance {
2152 WireContextInheritance::None => ContextInheritance::None,
2153 WireContextInheritance::SystemOnly => ContextInheritance::SystemOnly,
2154 WireContextInheritance::Full => ContextInheritance::Full,
2155 }
2156}
2157
2158fn parse_wire_role(label: &str) -> Option<WireRole> {
2162 match label {
2163 "explore" => Some(WireRole::Explore),
2164 "plan" => Some(WireRole::Plan),
2165 "implement" => Some(WireRole::Implement),
2166 "verify" => Some(WireRole::Verify),
2167 _ => None,
2168 }
2169}
2170
2171fn parse_wire_isolation(label: &str) -> Option<WireIsolation> {
2172 match label {
2173 "read_only" => Some(WireIsolation::ReadOnly),
2174 "worktree" => Some(WireIsolation::Worktree),
2175 "remote" => Some(WireIsolation::Remote),
2176 _ => None,
2177 }
2178}
2179
2180fn parse_wire_context_inheritance(label: &str) -> Option<WireContextInheritance> {
2181 match label {
2182 "none" => Some(WireContextInheritance::None),
2183 "system_only" => Some(WireContextInheritance::SystemOnly),
2184 "full" => Some(WireContextInheritance::Full),
2185 _ => None,
2186 }
2187}
2188
2189fn seed_initial_context(engine: &mut LoopStateMachine, initial: &InitialContext) {
2192 if !initial.messages.is_empty() {
2193 engine.preload_history(initial.messages.iter().map(logical_message).collect());
2194 }
2195 seed_knowledge(engine, &initial.knowledge);
2196 if !initial.requested_capabilities.is_empty() {
2197 engine.set_requested_capabilities(initial.requested_capabilities.clone());
2198 }
2199}
2200
2201fn seed_knowledge(engine: &mut LoopStateMachine, entries: &[super::root::KnowledgeEntry]) {
2207 if entries.is_empty() {
2208 return;
2209 }
2210 let entries: Vec<crate::mm::PageInEntry> = entries
2211 .iter()
2212 .map(|entry| crate::mm::PageInEntry {
2213 content: entry.content.clone(),
2214 tokens: entry.tokens,
2215 source: None,
2216 key: entry.key.clone(),
2217 pinned: entry.pinned,
2218 })
2219 .collect();
2220 engine.apply_page_in(&entries);
2221}
2222
2223fn runtime_signal(
2244 signal: &LogicalSignal,
2245 accepted_at_ms: WireU64,
2246) -> crate::types::signal::RuntimeSignal {
2247 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
2248
2249 let source = match signal.source {
2250 Some(SignalSourceKind::Cron) => SignalSource::Cron,
2251 Some(SignalSourceKind::Gateway) => SignalSource::Gateway,
2252 Some(SignalSourceKind::Heartbeat) => SignalSource::Heartbeat,
2253 Some(SignalSourceKind::Custom) | None => SignalSource::Custom,
2254 };
2255 let urgency = match signal.urgency {
2256 Some(SignalUrgency::Low) => Urgency::Low,
2257 Some(SignalUrgency::High) => Urgency::High,
2258 Some(SignalUrgency::Critical) => Urgency::Critical,
2259 Some(SignalUrgency::Normal) | None => Urgency::Normal,
2260 };
2261 let mut runtime = RuntimeSignal::new(
2262 source,
2263 SignalType::Event,
2268 urgency,
2269 signal_summary(signal),
2270 )
2271 .with_id(signal.signal_id.as_str())
2272 .with_payload(signal.payload.get().clone())
2273 .with_timestamp(accepted_at_ms.get());
2274 if let Some(key) = &signal.dedupe_key {
2275 runtime = runtime.with_dedupe(key.as_str());
2276 }
2277 if let Some(after) = signal.escalate_after_ms {
2282 runtime = runtime.with_deadline(accepted_at_ms.get().saturating_add(after.get()));
2283 }
2284 runtime
2285}
2286
2287fn signal_summary(signal: &LogicalSignal) -> String {
2293 const SIGNAL_SUMMARY_MAX_BYTES: usize = 512;
2294 match signal.payload.get() {
2295 serde_json::Value::Null => signal.signal_id.as_str().to_string(),
2296 serde_json::Value::String(text) => {
2297 truncate_on_char_boundary(text, SIGNAL_SUMMARY_MAX_BYTES)
2298 }
2299 other => truncate_on_char_boundary(&other.to_string(), SIGNAL_SUMMARY_MAX_BYTES),
2300 }
2301}
2302
2303fn live_policy_label(patch: &super::command::LivePolicyPatch) -> &'static str {
2304 use super::command::LivePolicyPatch;
2305 match patch {
2306 LivePolicyPatch::ReplaceSignalPolicy(_) => "signal",
2307 LivePolicyPatch::ReplaceGovernancePolicy(_) => "governance",
2308 LivePolicyPatch::TightenResourceQuota(_) => "resource_quota",
2309 LivePolicyPatch::ReplaceRecoveryPolicy(_) => "recovery",
2310 }
2311}
2312
2313fn logical_message(message: &super::root::LogicalMessage) -> Message {
2314 Message {
2315 role: core_role_of(message.role),
2316 content: Content::Text(message.content.clone()),
2317 tool_calls: Vec::new(),
2318 token_count: message.tokens,
2319 }
2320}
2321
2322fn core_role_of(role: MessageRole) -> Role {
2323 match role {
2324 MessageRole::System => Role::System,
2325 MessageRole::User => Role::User,
2326 MessageRole::Assistant => Role::Assistant,
2327 MessageRole::Tool => Role::Tool,
2328 }
2329}
2330
2331fn wire_role_of(role: Role) -> MessageRole {
2332 match role {
2333 Role::System => MessageRole::System,
2334 Role::User => MessageRole::User,
2335 Role::Assistant => MessageRole::Assistant,
2336 Role::Tool => MessageRole::Tool,
2337 }
2338}
2339
2340fn rendered_context(
2341 context: &crate::context::renderer::InternalRenderedContext,
2342) -> WireRenderedContext {
2343 WireRenderedContext {
2344 system_stable: context.system_stable.clone(),
2345 system_knowledge: context.system_knowledge.clone(),
2346 turns: context.turns.iter().map(provider_message).collect(),
2347 state_turn: context.state_turn.as_ref().map(provider_message),
2348 frozen_prefix_len: context.frozen_prefix_len.map(|len| len as u32),
2349 }
2350}
2351
2352fn provider_message(message: &Message) -> ProviderMessage {
2353 let (content, tool_call_id) = match &message.content {
2354 Content::Parts(parts) => match parts.as_slice() {
2355 [
2356 ContentPart::ToolResult {
2357 call_id, output, ..
2358 },
2359 ] => (output.clone(), Some(call_id.to_string())),
2360 _ => message_body_parts(message)
2361 .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
2362 .unwrap_or_default(),
2363 },
2364 Content::Text(_) => message_body_parts(message)
2365 .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
2366 .unwrap_or_default(),
2367 };
2368 ProviderMessage {
2369 role: wire_role_of(message.role),
2370 content,
2371 tool_calls: message
2372 .tool_calls
2373 .iter()
2374 .filter_map(|call| wire_tool_call(call).ok())
2375 .collect(),
2376 tool_call_id: tool_call_id.and_then(|call_id| super::scalar::CallId::new(call_id).ok()),
2377 tokens: message.token_count,
2378 }
2379}
2380
2381fn tool_schema(schema: &crate::types::message::ToolSchema) -> WireToolSchema {
2382 WireToolSchema {
2383 name: schema.name.to_string(),
2384 description: schema.description.clone(),
2385 parameters: super::scalar::BoundedJson::new(schema.parameters.clone())
2386 .unwrap_or_else(|_| Default::default()),
2387 }
2388}
2389
2390fn workflow_budget(budget: &crate::orchestration::workflow::WorkflowBudget) -> WireWorkflowBudget {
2391 WireWorkflowBudget {
2392 max_total_tokens: budget.tokens_max.map(WireU64::new),
2393 max_turns: None,
2394 max_concurrency: budget.max_concurrent_subagents.map(|max| max as u32),
2395 }
2396}
2397
2398fn sub_agent_result(completed: &ChildCompleted) -> SubAgentResult {
2399 let termination = match completed.result.status {
2400 ChildStatus::Completed => TerminationReason::Completed,
2401 ChildStatus::Failed => TerminationReason::Error,
2402 ChildStatus::Cancelled => TerminationReason::UserAbort,
2403 };
2404 SubAgentResult {
2405 agent_id: completed.task_id.as_str().into(),
2406 result: LoopResult {
2407 termination,
2408 final_message: completed
2409 .result
2410 .output
2411 .as_ref()
2412 .map(|text| Message::assistant(text.clone())),
2413 turns_used: completed
2414 .result
2415 .usage
2416 .as_ref()
2417 .and_then(|usage| usage.turns)
2418 .unwrap_or(0),
2419 total_tokens_used: completed
2420 .result
2421 .usage
2422 .as_ref()
2423 .and_then(|usage| usage.output_tokens)
2424 .map_or(0, WireU64::get),
2425 loop_continue: None,
2426 classify_branch: None,
2427 pace_decision: None,
2428 tournament_winner: None,
2429 },
2430 }
2431}
2432
2433fn attempt_ordinal(attempt_id: &AttemptId) -> Option<u32> {
2434 attempt_id.as_str().rsplit(':').next()?.parse().ok()
2435}
2436
2437fn supervision_label(policy: crate::scheduler::tcb::ChildFailurePolicy) -> &'static str {
2438 match policy {
2439 crate::scheduler::tcb::ChildFailurePolicy::Propagate => "propagate",
2440 crate::scheduler::tcb::ChildFailurePolicy::Isolate => "isolate",
2441 crate::scheduler::tcb::ChildFailurePolicy::Restart => "restart",
2442 crate::scheduler::tcb::ChildFailurePolicy::Retry => "retry",
2443 crate::scheduler::tcb::ChildFailurePolicy::Ignore => "ignore",
2444 }
2445}
2446
2447fn agent_terminal(result: &LoopResult) -> KernelTerminal {
2454 let usage = UsageReport {
2455 input_tokens: WireU64::new(result.total_tokens_used),
2456 output_tokens: WireU64::ZERO,
2457 turns: result.turns_used,
2458 cached_input_tokens: None,
2459 };
2460 let termination = match result.termination {
2461 TerminationReason::Completed => WireTermination::Completed,
2462 TerminationReason::MaxTurns => WireTermination::MaxTurns,
2463 TerminationReason::TokenBudget => WireTermination::TokenBudget,
2464 TerminationReason::Timeout => WireTermination::Deadline,
2465 TerminationReason::ContextOverflow => WireTermination::ContextOverflow,
2466 TerminationReason::NoProgress => WireTermination::NoProgress,
2467 TerminationReason::MilestoneExceeded => WireTermination::MilestoneExceeded,
2468 TerminationReason::UserAbort => {
2469 return KernelTerminal::Cancelled(CancelledTerminal {
2470 reason: CancellationReason::User,
2471 usage,
2472 });
2473 }
2474 TerminationReason::Error => {
2475 return KernelTerminal::Failed(FailedTerminal {
2476 failure: KernelFailure {
2477 code: KernelFailureCode::InvariantViolated,
2478 message: "the agent loop ended in an error state".to_string(),
2479 },
2480 usage,
2481 });
2482 }
2483 };
2484 KernelTerminal::Agent(AgentTerminal {
2485 result: WireLoopResult {
2486 termination,
2487 final_message: result.final_message.as_ref().map(provider_message),
2488 turns_used: result.turns_used,
2489 pace_decision: result.pace_decision.as_ref().map(|decision| {
2490 super::terminal::PaceDecision {
2491 action: match decision.action {
2492 CorePaceAction::Continue => super::terminal::PaceAction::Continue,
2493 CorePaceAction::Sleep => super::terminal::PaceAction::Sleep,
2494 CorePaceAction::Stop => super::terminal::PaceAction::Stop,
2495 },
2496 delay_ms: decision.delay_ms.map(WireU64::new),
2497 reason: decision.reason.clone(),
2498 coerced_from: decision.coerced_from.clone(),
2499 }
2500 }),
2501 },
2502 usage,
2503 })
2504}
2505
2506fn publishes(disposition: &StepDisposition, tag: EffectKindTag) -> bool {
2507 disposition
2508 .effects()
2509 .iter()
2510 .any(|effect| effect.tag() == tag)
2511}
2512
2513fn loop_action_label(action: &LoopAction) -> &'static str {
2514 match action {
2515 LoopAction::CallLLM { .. } => "call_provider",
2516 LoopAction::ExecuteTools { .. } => "execute_tools",
2517 LoopAction::RequestApproval { .. } => "request_approval",
2518 LoopAction::SpawnWorkflow { .. } => "spawn_tasks",
2519 LoopAction::PreemptSubAgents { .. } => "preempt_tasks",
2520 LoopAction::PersistMemory { .. } => "persist_memory",
2521 LoopAction::QueryMemory { .. } => "query_memory",
2522 LoopAction::ArchivePageOut { .. } => "archive_page_out",
2523 LoopAction::EvaluateMilestone { .. } => "evaluate_milestone",
2524 LoopAction::Done { .. } => "terminal",
2525 LoopAction::AwaitingResume => "awaiting_resume",
2526 }
2527}
2528
2529fn syscall_ack(name: &str) -> &'static str {
2534 match name {
2535 "start_workflow" => {
2536 "workflow accepted: its ready nodes are scheduled; each result arrives as that node \
2537 completes"
2538 }
2539 "submit_workflow_nodes" => {
2540 "nodes appended to the running workflow; each result arrives as that node completes"
2541 }
2542 "skill" => "skill activated: its guidance and tools are in this turn's context",
2543 "update_plan" => "plan updated: the new state renders in [TASK STATE] from here on",
2544 crate::context::manager::MEMORY_TOOL_NAME => {
2545 "memory search issued: matching records are added to this conversation before your \
2546 next turn"
2547 }
2548 crate::context::manager::READ_RESULT_TOOL_NAME => "page-in requested",
2549 "send_message" | "publish_channel" => "local handle routed",
2550 "receive_mailbox" | "receive_channel" | "read_object" => "local state returned",
2551 _ => "accepted",
2552 }
2553}
2554
2555fn validate_ipc_labels(message_id: &str, kind: &str) -> Result<(), SyscallRefusal> {
2556 if message_id.is_empty() || kind.is_empty() || message_id.len() > 256 || kind.len() > 256 {
2557 return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2558 "local_ipc",
2559 "message_id and message_kind must contain 1..=256 bytes",
2560 )));
2561 }
2562 Ok(())
2563}
2564
2565fn resolve_ipc_handle(
2566 engine: &LoopStateMachine,
2567 handle_id: &super::scalar::HandleId,
2568) -> Result<crate::mm::handle::Handle, SyscallRefusal> {
2569 engine
2570 .ctx
2571 .handles
2572 .all()
2573 .iter()
2574 .find(|handle| {
2575 handle.source.as_deref() == Some(handle_id.as_str())
2576 || handle.id.to_string() == handle_id.as_str()
2577 })
2578 .cloned()
2579 .ok_or_else(|| {
2580 SyscallRefusal::Rejected(SyscallRejection::new(
2581 "local_ipc",
2582 format!("payload handle {handle_id} is not reachable by this operation"),
2583 ))
2584 })
2585}
2586
2587fn local_ipc_refusal(error: crate::scheduler::tcb::LocalIpcError) -> SyscallRefusal {
2588 let reason = match error {
2589 crate::scheduler::tcb::LocalIpcError::UnknownCaller => "unknown caller",
2590 crate::scheduler::tcb::LocalIpcError::CallerTerminal => "caller is terminal",
2591 crate::scheduler::tcb::LocalIpcError::UnknownRecipient => "unknown recipient",
2592 crate::scheduler::tcb::LocalIpcError::ChannelSubscribersMismatch => {
2593 "channel subscriber set is immutable"
2594 }
2595 crate::scheduler::tcb::LocalIpcError::NotSubscriber => "caller is not a channel subscriber",
2596 crate::scheduler::tcb::LocalIpcError::Full => "IPC capacity is full",
2597 crate::scheduler::tcb::LocalIpcError::Expired => "message TTL already expired",
2598 crate::scheduler::tcb::LocalIpcError::ObjectConflict => {
2599 "object id already names a different descriptor"
2600 }
2601 };
2602 SyscallRefusal::Rejected(SyscallRejection::new("local_ipc", reason))
2603}
2604
2605fn local_ipc_outcome(accepted: bool) -> SyscallOutcome {
2606 SyscallOutcome {
2607 ack: Some(
2608 serde_json::json!({
2609 "status": if accepted { "accepted" } else { "duplicate" },
2610 })
2611 .to_string(),
2612 ),
2613 ..SyscallOutcome::default()
2614 }
2615}
2616
2617fn ipc_messages_outcome(messages: &[crate::scheduler::mailbox::MailboxMessage]) -> SyscallOutcome {
2618 SyscallOutcome {
2619 ack: Some(
2620 serde_json::to_string(messages)
2621 .expect("canonical mailbox messages are always serializable"),
2622 ),
2623 ..SyscallOutcome::default()
2624 }
2625}
2626
2627fn core_provider_message(message: &ProviderMessage) -> Result<Message, KernelFault> {
2629 Ok(Message {
2630 role: core_role_of(message.role),
2631 content: Content::Text(message.content.clone()),
2632 tool_calls: message.tool_calls.iter().map(core_tool_call).collect(),
2633 token_count: message.tokens,
2634 })
2635}
2636
2637fn core_tool_call(call: &WireToolCall) -> crate::types::message::ToolCall {
2638 crate::types::message::ToolCall {
2639 id: call.call_id.as_str().into(),
2640 name: call.name.as_str().into(),
2641 arguments: call.arguments.get().clone(),
2642 }
2643}
2644
2645fn wire_tool_call(call: &crate::types::message::ToolCall) -> Result<WireToolCall, KernelFault> {
2646 Ok(WireToolCall {
2647 call_id: super::scalar::CallId::new(call.id.as_str()).map_err(malformed)?,
2648 name: call.name.to_string(),
2649 arguments: super::scalar::BoundedJson::new(call.arguments.clone())
2650 .unwrap_or_else(|_| Default::default()),
2651 })
2652}
2653
2654fn wire_approval_request(
2655 request: &crate::scheduler::state_machine::ApprovalRequest,
2656) -> Result<WireApprovalRequest, KernelFault> {
2657 Ok(WireApprovalRequest {
2658 call_id: super::scalar::CallId::new(request.call_id.as_str()).map_err(malformed)?,
2659 tool_name: request.tool.clone(),
2660 arguments: super::scalar::BoundedJson::new(request.arguments.clone())
2661 .unwrap_or_else(|_| Default::default()),
2662 reason: (!request.reason.is_empty()).then(|| request.reason.clone()),
2663 })
2664}
2665
2666fn core_tool_result(payload: &WireToolResultPayload) -> ToolResult {
2688 let disposition = payload.disposition();
2689 let is_error = payload.is_error();
2690 let error_kind = match disposition {
2691 ToolResultDisposition::Fatal => Some(ToolErrorKind::Fatal),
2692 ToolResultDisposition::Recoverable => is_error.then_some(ToolErrorKind::Recoverable),
2693 };
2694 match payload {
2695 WireToolResultPayload::Inline(inline) => ToolResult {
2696 call_id: inline.call_id.as_str().into(),
2697 output: Content::Text(inline.result.output.clone()),
2698 durable_content: inline.result.durable_content.clone(),
2699 is_error,
2700 is_fatal: disposition.is_fatal(),
2701 error_kind,
2702 token_count: inline.result.tokens,
2703 },
2704 WireToolResultPayload::External(external) => ToolResult {
2705 call_id: external.call_id.as_str().into(),
2706 output: Content::Text(external.preview.clone()),
2707 durable_content: None,
2708 is_error,
2709 is_fatal: disposition.is_fatal(),
2710 error_kind,
2711 token_count: None,
2712 },
2713 }
2714}
2715
2716fn check_payload_policy(
2732 payload: &WireToolResultPayload,
2733 policy: &super::config::ResolvedPayloadPolicy,
2734) -> Result<(), KernelFault> {
2735 let threshold = policy.inline_threshold_bytes as u64;
2736 match payload {
2737 WireToolResultPayload::Inline(inline) => {
2738 let durable_size = inline
2739 .result
2740 .durable_content
2741 .as_ref()
2742 .map(|content| {
2743 content.validate().map_err(|error| {
2744 KernelFault::new(
2745 KernelFaultCode::MalformedEnvelope,
2746 format!(
2747 "inline tool result {} carries invalid durable content: {error}",
2748 inline.call_id
2749 ),
2750 )
2751 })?;
2752 serde_json::to_vec(content).map(|bytes| bytes.len() as u64).map_err(|error| {
2753 KernelFault::new(
2754 KernelFaultCode::MalformedEnvelope,
2755 format!(
2756 "inline tool result {} durable content cannot be encoded: {error}",
2757 inline.call_id
2758 ),
2759 )
2760 })
2761 })
2762 .transpose()?
2763 .unwrap_or(0);
2764 let size = (inline.result.output.len() as u64).max(durable_size);
2765 if size >= threshold {
2766 return Err(KernelFault::new(
2767 KernelFaultCode::ResourceLimitExceeded,
2768 format!(
2769 "tool result {} is {size} bytes and this operation's payload policy \
2770 externalises at {threshold}; the host persists the body and submits an \
2771 external result — the kernel does not spool on its behalf (§7.10)",
2772 inline.call_id
2773 ),
2774 ));
2775 }
2776 Ok(())
2777 }
2778 WireToolResultPayload::External(external) => {
2779 if !is_verifiable_digest(external.digest.as_str()) {
2780 return Err(KernelFault::new(
2781 KernelFaultCode::MalformedEnvelope,
2782 format!(
2783 "external tool result {} carries digest {}, which this kernel cannot \
2784 verify; a paged-in body is checked by recomputing {}:<64 hex> over it",
2785 external.call_id,
2786 external.digest,
2787 super::record::DIGEST_ALGORITHM
2788 ),
2789 ));
2790 }
2791 let size = external.original_size.get();
2792 if size < threshold {
2793 return Err(KernelFault::new(
2794 KernelFaultCode::MalformedEnvelope,
2795 format!(
2796 "external tool result {} declares {size} bytes but this operation's \
2797 payload policy inlines below {threshold}; the threshold is the single \
2798 arbiter of which arm a result takes (§7.10)",
2799 external.call_id
2800 ),
2801 ));
2802 }
2803 let preview = external.preview.len() as u64;
2804 if preview > policy.preview_bytes as u64 {
2805 return Err(KernelFault::new(
2806 KernelFaultCode::ResourceLimitExceeded,
2807 format!(
2808 "external tool result {} carries a {preview}-byte preview and this \
2809 operation keeps {} bytes resident",
2810 external.call_id, policy.preview_bytes
2811 ),
2812 ));
2813 }
2814 Ok(())
2815 }
2816 }
2817}
2818
2819fn is_verifiable_digest(digest: &str) -> bool {
2821 let Some(hex) = digest.strip_prefix(super::record::DIGEST_ALGORITHM) else {
2822 return false;
2823 };
2824 let Some(hex) = hex.strip_prefix(':') else {
2825 return false;
2826 };
2827 hex.len() == 64
2828 && hex
2829 .bytes()
2830 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
2831}
2832
2833fn core_milestone_result(
2834 result: &super::effect::MilestoneCheckResult,
2835) -> crate::types::milestone::MilestoneCheckResult {
2836 crate::types::milestone::MilestoneCheckResult {
2837 phase_id: result.phase_id.clone(),
2838 passed: result.passed,
2839 reason: (!result.passed).then(|| {
2840 if result.failed_criteria.is_empty() {
2841 result.notes.clone()
2842 } else {
2843 format!("unmet criteria: {}", result.failed_criteria.join("; "))
2844 }
2845 }),
2846 }
2847}
2848
2849fn core_milestone_contract(
2858 contract: &super::config::VerificationContract,
2859 config: &ResolvedOperationConfig,
2860) -> crate::types::milestone::MilestoneContract {
2861 use crate::types::capability::{CapabilityDescriptor, CapabilityKind as CoreCapabilityKind};
2862 use crate::types::milestone::{MilestoneContract, MilestonePhase};
2863
2864 let mut cascade = MilestoneContract::new();
2865 for phase in &contract.phases {
2866 let unlocks = phase
2867 .unlocks
2868 .iter()
2869 .map(|id| {
2870 if let Some(tool) = config.tool_catalog.iter().find(|tool| &tool.name == id) {
2871 CapabilityDescriptor::tool(core_tool_schema(tool))
2872 } else if let Some(skill) = config.skill_catalog.iter().find(|s| &s.name == id) {
2873 CapabilityDescriptor::skill(core_skill(skill))
2874 } else {
2875 CapabilityDescriptor::marker(
2876 CoreCapabilityKind::Tool,
2877 id.as_str(),
2878 String::new(),
2879 )
2880 }
2881 })
2882 .collect();
2883 cascade = cascade.phase(MilestonePhase {
2884 unlocks,
2885 ..MilestonePhase::new(phase.phase_id.clone())
2886 });
2887 }
2888 cascade
2889}
2890
2891fn core_memory_kind(kind: WireMemoryKind) -> crate::mm::memory::MemoryKind {
2892 match kind {
2893 WireMemoryKind::User => crate::mm::memory::MemoryKind::User,
2894 WireMemoryKind::Feedback => crate::mm::memory::MemoryKind::Feedback,
2895 WireMemoryKind::Project => crate::mm::memory::MemoryKind::Project,
2896 WireMemoryKind::Reference => crate::mm::memory::MemoryKind::Reference,
2897 }
2898}
2899
2900fn wire_memory_kind_label(kind: WireMemoryKind) -> &'static str {
2901 core_memory_kind(kind).label()
2902}
2903
2904fn binding_scope(binding_id: &MemoryBindingId) -> crate::mm::memory::MemoryScope {
2909 crate::mm::memory::MemoryScope::new(String::new(), binding_id.as_str().to_string())
2910}
2911
2912fn host_failure_text(failure: &HostEffectFailure) -> String {
2915 if failure.message.is_empty() {
2916 failure.kind.as_str().to_string()
2917 } else {
2918 format!("{}: {}", failure.kind.as_str(), failure.message)
2919 }
2920}
2921
2922fn unowned_resolution(effect_id: &EffectId, what: &str) -> KernelFault {
2926 KernelFault::new(
2927 KernelFaultCode::RecordCorrupted,
2928 format!(
2929 "effect {effect_id} resolves a {what} this runtime never authored; the driver's ledger \
2930 no longer describes the journal — rebuild from the records"
2931 ),
2932 )
2933}
2934
2935fn truncate_on_char_boundary(text: &str, max_bytes: usize) -> String {
2936 if text.len() <= max_bytes {
2937 return text.to_string();
2938 }
2939 let mut end = max_bytes;
2940 while end > 0 && !text.is_char_boundary(end) {
2941 end -= 1;
2942 }
2943 text[..end].to_string()
2944}
2945
2946fn build_engine(config: &ResolvedOperationConfig) -> LoopStateMachine {
2954 let execution = &config.execution_policy;
2955 let mut engine = LoopStateMachine::new(SchedulerBudget {
2956 max_tokens: execution.max_context_tokens,
2957 max_turns: execution.max_turns,
2958 max_total_tokens: execution.max_total_tokens.get(),
2959 max_wall_ms: execution.max_wall_ms.map(WireU64::get),
2960 });
2961 if let Some(grant) = config.budget_grant.clone() {
2962 engine.set_budget_grant(grant);
2963 }
2964 let scheduler_policy = config.scheduler_policy;
2965 engine.set_scheduler_policy(crate::scheduler::policy::SchedulerPolicyConfig {
2966 critical_path_weight: i64::from(scheduler_policy.critical_path_weight),
2967 fanout_weight: i64::from(scheduler_policy.fanout_weight),
2968 age_weight: i64::from(scheduler_policy.age_weight),
2969 token_cost_weight: i64::from(scheduler_policy.token_cost_weight),
2970 deadline_weight: i64::from(scheduler_policy.deadline_weight),
2971 process_priority_weight: i64::from(scheduler_policy.process_priority_weight),
2972 resource_pressure_weight: i64::from(scheduler_policy.resource_pressure_weight),
2973 budget_pressure_weight: i64::from(scheduler_policy.budget_pressure_weight),
2974 });
2975
2976 engine.set_criteria_gate(execution.criteria_gate_enabled);
2977 engine.set_repeat_fuse(crate::governance::repeat_fuse::RepeatFuseConfig {
2978 enabled: execution.repeat_fuse.enabled,
2979 deny_after: execution.repeat_fuse.deny_after,
2980 terminate_after: execution.repeat_fuse.terminate_after,
2981 });
2982 engine.set_entropy_watch(crate::scheduler::entropy::EntropyWatchConfig {
2983 enabled: execution.entropy_watch.enabled,
2984 threshold: f64::from(execution.entropy_watch.threshold_ppm.get()) / 1_000_000.0,
2985 hysteresis: f64::from(execution.entropy_watch.hysteresis_ppm.get()) / 1_000_000.0,
2986 cooldown_turns: execution.entropy_watch.cooldown_turns,
2987 notify_model: execution.entropy_watch.notify_model,
2988 });
2989 install_live_policies(&mut engine, config);
2990 engine
2991 .ctx
2992 .set_memory_enabled(config.feature_policy.memory_enabled);
2993 engine
2994 .ctx
2995 .set_knowledge_enabled(config.feature_policy.knowledge_enabled);
2996 engine
2997 .ctx
2998 .set_plan_tool_enabled(config.feature_policy.plan_tool_enabled);
2999 engine
3002 .ctx
3003 .set_available_skills(config.skill_catalog.iter().map(core_skill).collect());
3004 engine.ctx.set_stable_core_tools(
3005 config
3006 .feature_policy
3007 .stable_core_tool_ids
3008 .iter()
3009 .map(|id| id.as_str().into()),
3010 );
3011 engine.ctx.config.knowledge_budget_ratio =
3012 config.context_policy.knowledge_budget_ppm.as_ratio();
3013 engine.ctx.config.collapse_assistant_narration =
3014 config.context_policy.collapse_old_assistant_narration;
3015 engine.tools = config.tool_catalog.iter().map(core_tool_schema).collect();
3016 engine
3017}
3018
3019fn install_live_policies(engine: &mut LoopStateMachine, config: &ResolvedOperationConfig) {
3028 if let Some(quota) = core_quota(&config.resource_quota) {
3032 engine.set_resource_quota(quota);
3033 }
3034 if let Some(pipeline) = core_governance(&config.governance_policy) {
3038 engine.set_governance(pipeline);
3039 }
3040 engine.set_signal_policy(
3041 config.signal_policy.queue_max as usize,
3042 config.signal_policy.ttl_ms.map(WireU64::get),
3043 config.signal_policy.deadline_escalation,
3044 );
3045 engine.set_recovery_limits(
3051 config.recovery_policy.provider_recovery_attempts,
3052 config.recovery_policy.output_recovery_attempts,
3053 );
3054}
3055
3056fn core_quota(
3060 quota: &super::config::ResourceQuota,
3061) -> Option<crate::governance::quota::ResourceQuota> {
3062 if quota == &super::config::ResourceQuota::default() {
3063 return None;
3064 }
3065 Some(crate::governance::quota::ResourceQuota {
3066 max_concurrent_subagents: quota.max_concurrent_subagents,
3067 max_total_subagents: quota.max_total_subagents,
3068 max_spawn_depth: quota.max_spawn_depth,
3069 memory_writes_per_window: quota
3070 .memory_writes_per_window
3071 .as_ref()
3072 .map(|window| (window.max_events, window.window_ms.get())),
3073 max_workflow_nodes: quota.max_workflow_nodes.map(|max| max as usize),
3074 })
3075}
3076
3077fn core_governance(
3081 policy: &super::config::ResolvedGovernancePolicy,
3082) -> Option<crate::governance::pipeline::GovernancePipeline> {
3083 use super::command::{ParamConstraint as WireConstraint, PolicyAction};
3084 use crate::governance::constraint::{ConstraintRule, ParamConstraint as CoreConstraint};
3085 use crate::governance::permission::PermissionRule;
3086 use crate::governance::rate_limit::RateLimit;
3087
3088 if policy.default_action == PolicyAction::Allow
3089 && policy.rules.is_empty()
3090 && policy.vetoed_tools.is_empty()
3091 && policy.rate_limits.is_empty()
3092 && policy.constraints.is_empty()
3093 {
3094 return None;
3095 }
3096 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(core_policy_action(
3097 policy.default_action,
3098 ));
3099 for rule in &policy.rules {
3100 pipeline.permission.add_rule(PermissionRule {
3101 tool_pattern: rule.tool_pattern.as_str().into(),
3102 action: core_policy_action(rule.action),
3103 });
3104 }
3105 for tool in &policy.vetoed_tools {
3106 pipeline.veto.block_tool(tool.clone());
3107 }
3108 for limit in &policy.rate_limits {
3109 pipeline.rate_limiter.set_limit(
3110 limit.tool.clone(),
3111 RateLimit {
3112 max_calls: limit.max_calls,
3113 window_ms: limit.window_ms.get(),
3114 },
3115 );
3116 }
3117 for constraint in &policy.constraints {
3118 let rule = match constraint {
3119 WireConstraint::Required(_) => ConstraintRule::Required,
3120 WireConstraint::Enum(spec) => ConstraintRule::Enum(spec.values.clone()),
3121 WireConstraint::Range(spec) => ConstraintRule::Range {
3124 min: spec.min_micros.map(|micros| micros as f64 / 1_000_000.0),
3125 max: spec.max_micros.map(|micros| micros as f64 / 1_000_000.0),
3126 },
3127 };
3128 pipeline.constraints.add(CoreConstraint {
3129 tool_name: constraint.tool().to_string(),
3130 param_path: constraint.param_path().to_string(),
3131 rule,
3132 });
3133 }
3134 Some(pipeline)
3135}
3136
3137fn core_policy_action(
3138 action: super::command::PolicyAction,
3139) -> crate::governance::permission::PermissionAction {
3140 use crate::governance::permission::PermissionAction;
3141 match action {
3142 super::command::PolicyAction::Allow => PermissionAction::Allow,
3143 super::command::PolicyAction::Deny => PermissionAction::Deny,
3144 super::command::PolicyAction::AskUser => PermissionAction::AskUser,
3145 }
3146}
3147
3148fn core_skill(skill: &super::config::SkillMetadata) -> crate::types::skill::SkillMetadata {
3149 crate::types::skill::SkillMetadata {
3150 name: skill.name.as_str().into(),
3151 description: skill.description.clone(),
3152 when_to_use: skill.when_to_use.clone(),
3153 allowed_tools: skill
3154 .allowed_tools
3155 .iter()
3156 .map(|tool| tool.as_str().into())
3157 .collect(),
3158 capability_grants: skill.capability_grants.clone(),
3159 effort: skill.effort,
3160 estimated_tokens: skill.estimated_tokens.unwrap_or(0),
3161 }
3162}
3163
3164fn ensure_skill_grants_are_attenuated(
3165 grants: &[crate::types::capability::Capability],
3166 parent_capabilities: &[crate::types::capability::Capability],
3167) -> Result<(), Vec<crate::types::capability::Capability>> {
3168 crate::types::capability::caps_subset(grants, parent_capabilities)
3169}
3170
3171fn skill_grant_attenuation_message(
3172 skill_name: &str,
3173 violations: &[crate::types::capability::Capability],
3174) -> String {
3175 format!(
3176 "skill {skill_name:?} declares capability grants that would widen the mounting agent's authority: {}",
3177 violations
3178 .iter()
3179 .map(|capability| capability.id.0.as_str())
3180 .collect::<Vec<_>>()
3181 .join(", ")
3182 )
3183}
3184
3185fn core_tool_schema(schema: &WireToolSchema) -> crate::types::message::ToolSchema {
3186 crate::types::message::ToolSchema {
3187 name: schema.name.as_str().into(),
3188 description: schema.description.clone(),
3189 parameters: schema.parameters.get().clone(),
3190 }
3191}
3192
3193fn malformed(error: super::scalar::WireScalarError) -> KernelFault {
3194 KernelFault::new(KernelFaultCode::MalformedEnvelope, error.message)
3195}
3196
3197#[cfg(test)]
3198mod tests;