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