1use super::*;
2
3impl CanonicalOperationDriver {
4 pub fn project_logical_state(&self) -> LogicalStateProjection {
17 LogicalStateProjection {
18 root_kind: self.root_kind,
19 focus: self.focus.clone(),
20 syscall: self.project_syscall_state(),
21 scheduler: self.project_scheduler_state(),
22 context_vm: self.project_context_vm_state(),
23 }
24 }
25
26 pub(super) fn project_syscall_state(&self) -> SyscallState {
27 SyscallState {
28 policy_revision: self.policy.as_ref().map(LivePolicyState::revision),
29 live_config: self.policy.as_ref().map(|policy| policy.config().clone()),
30 provider_calls: self
31 .provider_calls
32 .iter()
33 .map(|(effect_id, call)| PendingProviderCallState {
34 effect_id: effect_id.clone(),
35 task_id: call.task_id.clone(),
36 exposed_tools: call.exposed_tools.iter().cloned().collect(),
37 })
38 .collect(),
39 consumed_call_ids: self.consumed_calls.iter().cloned().collect(),
40 authored_memory_writes: self
41 .pending_memory_writes
42 .iter()
43 .map(|(effect_id, write)| AuthoredMemoryWriteState {
44 effect_id: effect_id.clone(),
45 binding_id: write.binding_id.clone(),
46 name: write.name.clone(),
47 kind: write.kind,
48 size_bytes: write.size_bytes,
49 })
50 .collect(),
51 authored_memory_queries: self
52 .pending_memory_queries
53 .iter()
54 .map(|(effect_id, query)| AuthoredMemoryQueryState {
55 effect_id: effect_id.clone(),
56 binding_id: query.binding_id.clone(),
57 text: query.text.clone(),
58 requested_k: query.requested_k,
59 })
60 .collect(),
61 memory_write_window_ms: self
62 .engine
63 .as_ref()
64 .map(|engine| {
65 engine
66 .memory_write_window()
67 .iter()
68 .copied()
69 .map(WireU64::new)
70 .collect()
71 })
72 .unwrap_or_default(),
73 }
74 }
75
76 pub(super) fn project_scheduler_state(&self) -> SchedulerState {
77 let Some(engine) = self.engine.as_ref() else {
78 return SchedulerState::default();
79 };
80 let (total_tokens, subagents_spawned, rounds_completed) = engine.local_budget_usage();
81 let signal_state = engine.signal_checkpoint_state();
82 let entropy_state = engine.entropy_checkpoint_state();
83 let workflow = engine.workflow_checkpoint_nodes().map(|runtime_nodes| {
84 let workflow_id = self
85 .workflow_id
86 .as_ref()
87 .expect("an active canonical workflow has a logical identity");
88 assert_eq!(
89 runtime_nodes.len(),
90 self.workflow_nodes.len(),
91 "the semantic workflow and its canonical source DAG stay index-aligned"
92 );
93 WorkflowGraphState {
94 workflow_id: workflow_id.clone(),
95 nodes: runtime_nodes
96 .into_iter()
97 .zip(self.workflow_nodes.iter())
98 .map(|(runtime, wire)| WorkflowNodeState {
99 node_id: wire.node_id.clone(),
100 task: wire.task.clone(),
101 depends_on: wire.depends_on.clone(),
102 run_spec: wire.run_spec.clone(),
103 kind: workflow_kind_label(&runtime).to_string(),
104 status: workflow_status_label(runtime.status).to_string(),
105 active_agent_id: runtime.active_agent_id,
106 iterations_completed: runtime.iterations_completed as u32,
107 })
108 .collect(),
109 }
110 });
111 SchedulerState {
112 run_spec: engine.run_spec.as_ref().map(logical_agent_run_spec),
113 advertised_tool_ids: engine.advertised_tool_ids(),
114 turn: engine.turn,
115 total_tokens: WireU64::new(total_tokens),
116 rounds_completed,
117 subagents_spawned,
118 started_at_ms: engine.started_at_ms().map(WireU64::new),
119 wall_budget_ms: engine.wall_budget().map(WireU64::new),
120 tasks: engine
121 .task_table()
122 .all()
123 .iter()
124 .map(|tcb| TaskControlState {
125 task_id: TaskId::new(tcb.id.as_str())
126 .expect("an internal task id is always a legal branded ref"),
127 parent_task_id: tcb
128 .parent
129 .as_ref()
130 .and_then(|parent| TaskId::new(parent.as_str()).ok()),
131 lifecycle: tcb.state.label().to_string(),
132 runnable_cause: tcb.runnable_cause,
133 termination: match tcb.state {
134 TaskLifecycle::Done(reason) => Some(reason.label().to_string()),
135 _ => None,
136 },
137 wait_set: tcb.wait_set.as_ref().map(project_wait_set),
138 capability_ids: tcb.caps.iter().map(|cap| cap.to_string()).collect(),
139 capabilities: tcb.capabilities.clone(),
140 process: tcb.proc.as_ref().map(|process| ChildProcessState {
141 role: agent_role_label(process.role).to_string(),
142 isolation: agent_isolation_label(process.isolation).to_string(),
143 context_inheritance: context_inheritance_label(process.context_inheritance)
144 .to_string(),
145 join_result: process.result.as_ref().map(|result| {
146 super::super::scalar::BoundedJson::new(
147 serde_json::to_value(result)
148 .expect("a child join result is serializable"),
149 )
150 .expect("a child join result is bounded")
151 }),
152 }),
153 supervision: tcb.supervision.clone(),
154 supervision_events: tcb.supervision_events.clone(),
155 tokens_used: WireU64::new(tcb.budget.total_tokens),
156 turns_used: tcb.budget.turns,
157 child_budget_remaining: tcb.child_budget_remaining,
158 budget_grant: tcb.budget_grant.clone(),
159 mailbox: tcb.mailbox.clone(),
160 })
161 .collect(),
162 attempts: self
163 .attempts
164 .iter()
165 .filter_map(|(task_id, attempt_id)| {
166 Some(TaskAttemptState {
167 task_id: TaskId::new(task_id.as_str()).ok()?,
168 attempt_id: attempt_id.clone(),
169 })
170 })
171 .collect(),
172 workflow,
173 queued_signals: signal_state
174 .queued
175 .into_iter()
176 .map(|queued| queued_signal_state(&queued))
177 .collect(),
178 signal_dedupe_keys: signal_state
179 .seen_order
180 .into_iter()
181 .map(|key| key.to_string())
182 .collect(),
183 milestone: self
184 .loaded_contract_id
185 .as_ref()
186 .map(|contract_id| MilestoneState {
187 contract_id: contract_id.clone(),
188 phase_id: engine.current_milestone_phase_id().map(str::to_string),
189 complete: engine.is_milestone_complete(),
190 blocked_count: engine.milestone_blocked_count(),
191 }),
192 entropy: EntropyState {
193 window: entropy_state
194 .window
195 .into_iter()
196 .map(|entry| EntropyTurnState {
197 errored_results: entry.errored_results,
198 total_results: entry.total_results,
199 rollbacks: entry.rollbacks,
200 })
201 .collect(),
202 rollbacks_pending: entropy_state.rollbacks_pending,
203 disarmed: entropy_state.disarmed,
204 last_alert_turn: entropy_state.last_alert_turn,
205 },
206 channels: engine
207 .task_table()
208 .channels()
209 .iter()
210 .map(|(channel_id, channel)| LocalChannelState {
211 channel_id: channel_id.0.to_string(),
212 channel: channel.clone(),
213 })
214 .collect(),
215 objects: engine.task_table().objects().values().cloned().collect(),
216 }
217 }
218
219 pub(super) fn project_context_vm_state(&self) -> ContextVmState {
220 let Some(engine) = self.engine.as_ref() else {
221 return ContextVmState::default();
222 };
223 let ctx = &engine.ctx;
224 ContextVmState {
225 state_generation: ctx.state_generation(),
226 system_measurements: ctx.partitions.system.measurements.clone(),
227 history_measurements: ctx.partitions.history.measurements.clone(),
228 knowledge_reference_step: ctx.knowledge_checkpoint_state().0,
229 knowledge_budget_warned: ctx.knowledge_checkpoint_state().1,
230 handles: ctx
231 .handles
232 .all()
233 .iter()
234 .map(|handle| {
235 let (payload_ref, digest, original_size) = match &handle.residency {
236 Residency::External {
237 payload_ref,
238 digest,
239 original_size,
240 } => (
241 Some(payload_ref.clone()),
242 Some(digest.clone()),
243 Some(WireU64::new(*original_size)),
244 ),
245 Residency::PagedOut {
246 payload_ref,
247 digest,
248 } => (Some(payload_ref.clone()), Some(digest.clone()), None),
249 Residency::Resident | Residency::Collapsed => (None, None, None),
250 };
251 HandleState {
252 handle_id: handle.id,
253 kind: handle_kind_label(&handle.kind).to_string(),
254 residency: handle.residency.label().to_string(),
255 payload_ref,
256 digest,
257 original_size,
258 tokens: handle.tokens,
259 source: handle.source.as_ref().map(|source| source.to_string()),
260 }
261 })
262 .collect(),
263 next_handle_id: ctx.next_handle_id(),
264 pending_payload_loads: self
265 .pending_payload_loads
266 .iter()
267 .map(|(effect_id, load)| PendingPayloadLoadState {
268 effect_id: effect_id.clone(),
269 handle_id: load.handle_id.clone(),
270 digest: load.digest.clone(),
271 original_size: load.original_size.map(WireU64::new),
272 })
273 .collect(),
274 active_skills: ctx
275 .active_skills
276 .iter()
277 .map(|(skill, lease)| SkillLeaseState {
278 skill: skill.to_string(),
279 lease_until_turn: *lease,
280 })
281 .collect(),
282 knowledge: ctx
283 .partitions
284 .knowledge
285 .entries
286 .iter()
287 .map(|entry| KnowledgeSlotState {
288 key: entry.key.as_ref().map(|key| key.to_string()),
289 role: role_label(entry.message.role).to_string(),
290 body: self.project_body(&entry.message),
291 tokens: entry.tokens,
292 pinned: entry.pinned,
293 evict_at_boundary: entry.evict_at_boundary,
294 tool_calls: entry
295 .message
296 .tool_calls
297 .iter()
298 .map(|call| LogicalToolCall {
299 call_id: call.id.to_string(),
300 name: call.name.to_string(),
301 arguments: call.arguments.to_string(),
302 })
303 .collect(),
304 pending: entry.pending.as_ref().map(|pending| {
305 Box::new(self.project_message(
306 MessagePartition::System,
307 &pending.0,
308 pending.1,
309 ))
310 }),
311 use_count: entry.use_count,
312 last_used_step: entry.last_used_step,
313 })
314 .collect(),
315 signals: ctx.partitions.signals.clone(),
316 messages: ctx
317 .partitions
318 .system
319 .messages
320 .iter()
321 .enumerate()
322 .map(|(index, message)| {
323 self.project_message(
324 MessagePartition::System,
325 message,
326 ctx.partitions
327 .system
328 .measurements
329 .get(index)
330 .map(|measurement| measurement.tokens)
331 .unwrap_or_else(|| ctx.engine.count_message(message)),
332 )
333 })
334 .chain(ctx.partitions.history.messages.iter().enumerate().map(
335 |(index, message)| {
336 self.project_message(
337 MessagePartition::History,
338 message,
339 ctx.partitions
340 .history
341 .measurements
342 .get(index)
343 .map(|measurement| measurement.tokens)
344 .unwrap_or_else(|| ctx.engine.count_message(message)),
345 )
346 },
347 ))
348 .collect(),
349 task_state: project_task_state(&ctx.partitions.task_state),
350 partition_tokens: PartitionTokenState {
351 system: ctx.partitions.system.token_count,
352 knowledge: ctx.partitions.knowledge.token_count,
353 history: ctx.partitions.history.token_count,
354 },
355 history_len: ctx.partitions.history.messages.len() as u32,
356 frozen_history_len: ctx.frozen_history_len() as u32,
357 last_activity_ms: WireU64::new(ctx.last_activity_ms),
358 last_compact_ms: ctx.last_compact_ms.map(WireU64::new),
359 }
360 }
361
362 pub(super) fn project_message(
364 &self,
365 partition: MessagePartition,
366 message: &CoreMessage,
367 tokens: u32,
368 ) -> StoredMessageState {
369 StoredMessageState {
370 partition,
371 role: role_label(message.role).to_string(),
372 body: self.project_body(message),
373 tool_calls: message
374 .tool_calls
375 .iter()
376 .map(|call| LogicalToolCall {
377 call_id: call.id.to_string(),
378 name: call.name.to_string(),
379 arguments: call.arguments.to_string(),
380 })
381 .collect(),
382 tokens,
383 }
384 }
385
386 pub(super) fn project_body(&self, message: &CoreMessage) -> StoredMessageBody {
394 let single_tool_result_is_external =
398 match &message.content {
399 Content::Parts(parts) if parts.len() == 1 => match &parts[0] {
400 ContentPart::ToolResult { call_id, .. } => {
401 self.engine
402 .as_ref()
403 .and_then(|engine| {
404 engine.ctx.handles.all().iter().find(|handle| {
405 handle.source.as_deref() == Some(call_id.as_str())
406 })
407 })
408 .is_some_and(|handle| handle.residency.digest().is_some())
409 }
410 _ => false,
411 },
412 _ => false,
413 };
414 if !single_tool_result_is_external {
415 if let Some(results) = durable_tool_results_from_content(&message.content) {
416 return StoredMessageBody::Structured(StructuredMessageBody {
417 durable_content: None,
418 durable_tool_results: results,
419 });
420 }
421 if let Some(result) = durable_tool_result_from_content(&message.content) {
422 return StoredMessageBody::Structured(StructuredMessageBody {
423 durable_content: None,
424 durable_tool_results: vec![result],
425 });
426 }
427 }
428 let Some((text, tool_call_id, is_error)) = message_body_parts(message) else {
429 let durable_content = content_to_durable(&message.content)
430 .expect("every non-tool message uses the canonical durable content model");
431 return StoredMessageBody::Structured(StructuredMessageBody {
432 durable_content: Some(durable_content),
433 durable_tool_results: Vec::new(),
434 });
435 };
436 let Some(call_id) = tool_call_id.as_deref() else {
437 return StoredMessageBody::Inline(InlineMessageBody {
438 text,
439 tool_call_id,
440 is_error,
441 });
442 };
443 let referenced = self.engine.as_ref().and_then(|engine| {
444 let handle = engine
445 .ctx
446 .handles
447 .all()
448 .iter()
449 .find(|handle| handle.source.as_deref() == Some(call_id))?;
450 let digest = handle.residency.digest()?;
451 Some((
452 handle.id,
453 digest.to_string(),
454 matches!(handle.residency, Residency::PagedOut { .. }),
455 ))
456 });
457 match referenced {
458 Some((handle_id, digest, is_paged_out)) => {
459 StoredMessageBody::Reference(ReferencedMessageBody {
460 handle_id,
461 digest,
462 preview: if is_paged_out {
463 crate::context::renderer::collapse_preview(&text, call_id)
464 } else {
465 truncate_on_char_boundary(&text, self.preview_bytes())
466 },
467 tool_call_id,
468 is_error,
469 })
470 }
471 None => StoredMessageBody::Inline(InlineMessageBody {
472 text,
473 tool_call_id,
474 is_error,
475 }),
476 }
477 }
478
479 pub(super) fn preview_bytes(&self) -> usize {
480 self.policy
481 .as_ref()
482 .map(|policy| policy.config().payload_policy.preview_bytes as usize)
483 .unwrap_or(2 * 1024)
484 }
485
486 pub fn restore_logical_state(
501 genesis_config: &ResolvedOperationConfig,
502 state: &LogicalKernelState,
503 ) -> Result<Self, KernelFault> {
504 let mut driver = Self::new();
505 let live_config = state
506 .syscall
507 .live_config
508 .clone()
509 .unwrap_or_else(|| genesis_config.clone());
510
511 let mut engine = build_engine(genesis_config);
515 install_live_policies(&mut engine, &live_config);
516 engine.set_root_workflow(state.transition.root_kind == Some(RootKind::Workflow));
517 driver.policy = Some(LivePolicyState::restore(
518 state.syscall.policy_revision.unwrap_or(WireU64::ZERO),
519 live_config.clone(),
520 ));
521
522 restore_scheduler(&mut engine, &live_config, &state.scheduler)?;
523 if let Some(preempt) =
524 state
525 .transition
526 .pending_effects
527 .iter()
528 .find_map(|effect| match &effect.effect {
529 EffectKind::PreemptTasks(preempt) => Some(preempt),
530 _ => None,
531 })
532 {
533 engine.restore_pending_preempt(
534 preempt
535 .attempts
536 .iter()
537 .map(|attempt| attempt.task_id.as_str().to_string())
538 .collect(),
539 preempt.reason.clone(),
540 );
541 }
542 restore_context_vm(&mut engine, &state.context_vm)?;
543 engine.restore_memory_write_window(
544 state
545 .syscall
546 .memory_write_window_ms
547 .iter()
548 .map(|at| at.get())
549 .collect(),
550 );
551
552 if let Some(milestone) = &state.scheduler.milestone {
553 let contract = live_config
554 .verification_contract(&milestone.contract_id)
555 .ok_or_else(|| {
556 KernelFault::new(
557 KernelFaultCode::CheckpointIncompatible,
558 format!(
559 "the checkpoint runs verification contract {:?}, which this \
560 operation's configuration no longer declares",
561 milestone.contract_id
562 ),
563 )
564 })?;
565 engine.load_milestone_contract(core_milestone_contract(contract, &live_config));
566 if !engine
567 .restore_milestone_cursor(milestone.phase_id.as_deref(), milestone.blocked_count)
568 {
569 return Err(KernelFault::new(
570 KernelFaultCode::CheckpointIncompatible,
571 format!(
572 "the checkpoint sits on milestone phase {:?} of contract {:?}, which that \
573 contract does not declare",
574 milestone.phase_id, milestone.contract_id
575 ),
576 ));
577 }
578 driver.loaded_contract_id = Some(milestone.contract_id.clone());
579 }
580
581 driver.engine = Some(engine);
582 driver.root_kind = state.transition.root_kind;
583 driver.focus = state.transition.focus.clone();
584 if let Some(workflow) = &state.scheduler.workflow {
585 driver.workflow_id = Some(workflow.workflow_id.clone());
586 driver.node_ids = workflow
587 .nodes
588 .iter()
589 .map(|node| node.node_id.clone())
590 .collect();
591 driver.workflow_nodes = workflow
592 .nodes
593 .iter()
594 .map(|node| WireNode {
595 node_id: node.node_id.clone(),
596 task: node.task.clone(),
597 depends_on: node.depends_on.clone(),
598 run_spec: node.run_spec.clone(),
599 })
600 .collect();
601 }
602 driver.attempts = state
603 .scheduler
604 .attempts
605 .iter()
606 .map(|attempt| {
607 (
608 attempt.task_id.as_str().to_string(),
609 attempt.attempt_id.clone(),
610 )
611 })
612 .collect();
613 driver.provider_calls = state
614 .syscall
615 .provider_calls
616 .iter()
617 .map(|call| {
618 (
619 call.effect_id.clone(),
620 PendingProviderCall {
621 task_id: call.task_id.clone(),
622 exposed_tools: call.exposed_tools.iter().cloned().collect(),
623 },
624 )
625 })
626 .collect();
627 driver.consumed_calls = state.syscall.consumed_call_ids.iter().cloned().collect();
628 driver.pending_memory_writes = state
629 .syscall
630 .authored_memory_writes
631 .iter()
632 .map(|write| {
633 (
634 write.effect_id.clone(),
635 AuthoredMemoryWrite {
636 binding_id: write.binding_id.clone(),
637 name: write.name.clone(),
638 kind: write.kind,
639 size_bytes: write.size_bytes,
640 },
641 )
642 })
643 .collect();
644 driver.pending_memory_queries = state
645 .syscall
646 .authored_memory_queries
647 .iter()
648 .map(|query| {
649 (
650 query.effect_id.clone(),
651 AuthoredMemoryQuery {
652 binding_id: query.binding_id.clone(),
653 text: query.text.clone(),
654 requested_k: query.requested_k,
655 },
656 )
657 })
658 .collect();
659 driver.pending_payload_loads = state
660 .context_vm
661 .pending_payload_loads
662 .iter()
663 .map(|load| {
664 (
665 load.effect_id.clone(),
666 PendingPayloadLoad {
667 handle_id: load.handle_id.clone(),
668 digest: load.digest.clone(),
669 original_size: load.original_size.map(WireU64::get),
670 },
671 )
672 })
673 .collect();
674 Ok(driver)
675 }
676}