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 handles: ctx
226 .handles
227 .all()
228 .iter()
229 .map(|handle| {
230 let (payload_ref, digest, original_size) = match &handle.residency {
231 Residency::External {
232 payload_ref,
233 digest,
234 original_size,
235 } => (
236 Some(payload_ref.clone()),
237 Some(digest.clone()),
238 Some(WireU64::new(*original_size)),
239 ),
240 Residency::PagedOut {
241 payload_ref,
242 digest,
243 } => (Some(payload_ref.clone()), Some(digest.clone()), None),
244 Residency::Resident | Residency::Collapsed => (None, None, None),
245 };
246 HandleState {
247 handle_id: handle.id,
248 kind: handle_kind_label(&handle.kind).to_string(),
249 residency: handle.residency.label().to_string(),
250 payload_ref,
251 digest,
252 original_size,
253 tokens: handle.tokens,
254 source: handle.source.as_ref().map(|source| source.to_string()),
255 }
256 })
257 .collect(),
258 next_handle_id: ctx.next_handle_id(),
259 pending_payload_loads: self
260 .pending_payload_loads
261 .iter()
262 .map(|(effect_id, load)| PendingPayloadLoadState {
263 effect_id: effect_id.clone(),
264 handle_id: load.handle_id.clone(),
265 digest: load.digest.clone(),
266 original_size: load.original_size.map(WireU64::new),
267 })
268 .collect(),
269 active_skills: ctx
270 .active_skills
271 .iter()
272 .map(|(skill, lease)| SkillLeaseState {
273 skill: skill.to_string(),
274 lease_until_turn: *lease,
275 })
276 .collect(),
277 knowledge: ctx
278 .partitions
279 .knowledge
280 .entries
281 .iter()
282 .map(|entry| KnowledgeSlotState {
283 key: entry.key.as_ref().map(|key| key.to_string()),
284 role: role_label(entry.message.role).to_string(),
285 body: self.project_body(&entry.message),
286 tokens: entry.tokens,
287 pinned: entry.pinned,
288 evict_at_boundary: entry.evict_at_boundary,
289 })
290 .collect(),
291 signals: ctx.partitions.signals.clone(),
292 messages: ctx
293 .partitions
294 .system
295 .messages
296 .iter()
297 .map(|message| self.project_message(MessagePartition::System, message))
298 .chain(
299 ctx.partitions
300 .history
301 .messages
302 .iter()
303 .map(|message| self.project_message(MessagePartition::History, message)),
304 )
305 .collect(),
306 task_state: project_task_state(&ctx.partitions.task_state),
307 partition_tokens: PartitionTokenState {
308 system: ctx.partitions.system.token_count,
309 knowledge: ctx.partitions.knowledge.token_count,
310 history: ctx.partitions.history.token_count,
311 },
312 history_len: ctx.partitions.history.messages.len() as u32,
313 frozen_history_len: ctx.frozen_history_len() as u32,
314 last_activity_ms: WireU64::new(ctx.last_activity_ms),
315 last_compact_ms: ctx.last_compact_ms.map(WireU64::new),
316 }
317 }
318
319 pub(super) fn project_message(
321 &self,
322 partition: MessagePartition,
323 message: &Message,
324 ) -> StoredMessageState {
325 StoredMessageState {
326 partition,
327 role: role_label(message.role).to_string(),
328 body: self.project_body(message),
329 tool_calls: message
330 .tool_calls
331 .iter()
332 .map(|call| LogicalToolCall {
333 call_id: call.id.to_string(),
334 name: call.name.to_string(),
335 arguments: call.arguments.to_string(),
336 })
337 .collect(),
338 tokens: message.token_count.unwrap_or(0),
339 }
340 }
341
342 pub(super) fn project_body(&self, message: &Message) -> StoredMessageBody {
350 let single_tool_result_is_external =
354 match &message.content {
355 Content::Parts(parts) if parts.len() == 1 => match &parts[0] {
356 ContentPart::ToolResult { call_id, .. } => {
357 self.engine
358 .as_ref()
359 .and_then(|engine| {
360 engine.ctx.handles.all().iter().find(|handle| {
361 handle.source.as_deref() == Some(call_id.as_str())
362 })
363 })
364 .is_some_and(|handle| handle.residency.digest().is_some())
365 }
366 _ => false,
367 },
368 _ => false,
369 };
370 if !single_tool_result_is_external {
371 if let Some(results) = durable_tool_results_from_content(&message.content) {
372 return StoredMessageBody::Structured(StructuredMessageBody {
373 durable_content: None,
374 durable_tool_results: results,
375 });
376 }
377 if let Some(result) = durable_tool_result_from_content(&message.content) {
378 return StoredMessageBody::Structured(StructuredMessageBody {
379 durable_content: None,
380 durable_tool_results: vec![result],
381 });
382 }
383 }
384 let Some((text, tool_call_id, is_error)) = message_body_parts(message) else {
385 let durable_content = content_to_durable(&message.content)
386 .expect("every non-tool message uses the canonical durable content model");
387 return StoredMessageBody::Structured(StructuredMessageBody {
388 durable_content: Some(durable_content),
389 durable_tool_results: Vec::new(),
390 });
391 };
392 let Some(call_id) = tool_call_id.as_deref() else {
393 return StoredMessageBody::Inline(InlineMessageBody {
394 text,
395 tool_call_id,
396 is_error,
397 });
398 };
399 let referenced = self.engine.as_ref().and_then(|engine| {
400 let handle = engine
401 .ctx
402 .handles
403 .all()
404 .iter()
405 .find(|handle| handle.source.as_deref() == Some(call_id))?;
406 let digest = handle.residency.digest()?;
407 Some((
408 handle.id,
409 digest.to_string(),
410 matches!(handle.residency, Residency::PagedOut { .. }),
411 ))
412 });
413 match referenced {
414 Some((handle_id, digest, is_paged_out)) => {
415 StoredMessageBody::Reference(ReferencedMessageBody {
416 handle_id,
417 digest,
418 preview: if is_paged_out {
419 crate::context::renderer::collapse_preview(&text, call_id)
420 } else {
421 truncate_on_char_boundary(&text, self.preview_bytes())
422 },
423 tool_call_id,
424 is_error,
425 })
426 }
427 None => StoredMessageBody::Inline(InlineMessageBody {
428 text,
429 tool_call_id,
430 is_error,
431 }),
432 }
433 }
434
435 pub(super) fn preview_bytes(&self) -> usize {
436 self.policy
437 .as_ref()
438 .map(|policy| policy.config().payload_policy.preview_bytes as usize)
439 .unwrap_or(2 * 1024)
440 }
441
442 pub fn restore_logical_state(
457 genesis_config: &ResolvedOperationConfig,
458 state: &LogicalKernelState,
459 ) -> Result<Self, KernelFault> {
460 let mut driver = Self::new();
461 let live_config = state
462 .syscall
463 .live_config
464 .clone()
465 .unwrap_or_else(|| genesis_config.clone());
466
467 let mut engine = build_engine(genesis_config);
471 install_live_policies(&mut engine, &live_config);
472 engine.set_root_workflow(state.transition.root_kind == Some(RootKind::Workflow));
473 driver.policy = Some(LivePolicyState::restore(
474 state.syscall.policy_revision.unwrap_or(WireU64::ZERO),
475 live_config.clone(),
476 ));
477
478 restore_scheduler(&mut engine, &live_config, &state.scheduler)?;
479 if let Some(preempt) =
480 state
481 .transition
482 .pending_effects
483 .iter()
484 .find_map(|effect| match &effect.effect {
485 EffectKind::PreemptTasks(preempt) => Some(preempt),
486 _ => None,
487 })
488 {
489 engine.restore_pending_preempt(
490 preempt
491 .attempts
492 .iter()
493 .map(|attempt| attempt.task_id.as_str().to_string())
494 .collect(),
495 preempt.reason.clone(),
496 );
497 }
498 restore_context_vm(&mut engine, &state.context_vm)?;
499 engine.restore_memory_write_window(
500 state
501 .syscall
502 .memory_write_window_ms
503 .iter()
504 .map(|at| at.get())
505 .collect(),
506 );
507
508 if let Some(milestone) = &state.scheduler.milestone {
509 let contract = live_config
510 .verification_contract(&milestone.contract_id)
511 .ok_or_else(|| {
512 KernelFault::new(
513 KernelFaultCode::CheckpointIncompatible,
514 format!(
515 "the checkpoint runs verification contract {:?}, which this \
516 operation's configuration no longer declares",
517 milestone.contract_id
518 ),
519 )
520 })?;
521 engine.load_milestone_contract(core_milestone_contract(contract, &live_config));
522 if !engine
523 .restore_milestone_cursor(milestone.phase_id.as_deref(), milestone.blocked_count)
524 {
525 return Err(KernelFault::new(
526 KernelFaultCode::CheckpointIncompatible,
527 format!(
528 "the checkpoint sits on milestone phase {:?} of contract {:?}, which that \
529 contract does not declare",
530 milestone.phase_id, milestone.contract_id
531 ),
532 ));
533 }
534 driver.loaded_contract_id = Some(milestone.contract_id.clone());
535 }
536
537 driver.engine = Some(engine);
538 driver.root_kind = state.transition.root_kind;
539 driver.focus = state.transition.focus.clone();
540 if let Some(workflow) = &state.scheduler.workflow {
541 driver.workflow_id = Some(workflow.workflow_id.clone());
542 driver.node_ids = workflow
543 .nodes
544 .iter()
545 .map(|node| node.node_id.clone())
546 .collect();
547 driver.workflow_nodes = workflow
548 .nodes
549 .iter()
550 .map(|node| WireNode {
551 node_id: node.node_id.clone(),
552 task: node.task.clone(),
553 depends_on: node.depends_on.clone(),
554 run_spec: node.run_spec.clone(),
555 })
556 .collect();
557 }
558 driver.attempts = state
559 .scheduler
560 .attempts
561 .iter()
562 .map(|attempt| {
563 (
564 attempt.task_id.as_str().to_string(),
565 attempt.attempt_id.clone(),
566 )
567 })
568 .collect();
569 driver.provider_calls = state
570 .syscall
571 .provider_calls
572 .iter()
573 .map(|call| {
574 (
575 call.effect_id.clone(),
576 PendingProviderCall {
577 task_id: call.task_id.clone(),
578 exposed_tools: call.exposed_tools.iter().cloned().collect(),
579 },
580 )
581 })
582 .collect();
583 driver.consumed_calls = state.syscall.consumed_call_ids.iter().cloned().collect();
584 driver.pending_memory_writes = state
585 .syscall
586 .authored_memory_writes
587 .iter()
588 .map(|write| {
589 (
590 write.effect_id.clone(),
591 AuthoredMemoryWrite {
592 binding_id: write.binding_id.clone(),
593 name: write.name.clone(),
594 kind: write.kind,
595 size_bytes: write.size_bytes,
596 },
597 )
598 })
599 .collect();
600 driver.pending_memory_queries = state
601 .syscall
602 .authored_memory_queries
603 .iter()
604 .map(|query| {
605 (
606 query.effect_id.clone(),
607 AuthoredMemoryQuery {
608 binding_id: query.binding_id.clone(),
609 text: query.text.clone(),
610 requested_k: query.requested_k,
611 },
612 )
613 })
614 .collect();
615 driver.pending_payload_loads = state
616 .context_vm
617 .pending_payload_loads
618 .iter()
619 .map(|load| {
620 (
621 load.effect_id.clone(),
622 PendingPayloadLoad {
623 handle_id: load.handle_id.clone(),
624 digest: load.digest.clone(),
625 original_size: load.original_size.map(WireU64::get),
626 },
627 )
628 })
629 .collect();
630 Ok(driver)
631 }
632}