Skip to main content

deepstrike_core/runtime/kernel/wire/driver/
projection.rs

1use super::*;
2
3impl CanonicalOperationDriver {
4    // ----- §12.1 · the logical-state projection -----
5
6    /// Project the three driver-owned partitions of §12.1, plus the two transition fields the
7    /// driver rather than the transaction owns.
8    ///
9    /// Explicitly a **projection**, not a serialisation: every value below is read through a named
10    /// accessor and written into a canonical DTO field. That is the whole point of §12.1 — adding a
11    /// field to [`LoopStateMachine`] must not change the checkpoint format, and a checkpoint field
12    /// must not silently vanish because an internal one was renamed. It is also why the internal
13    /// enums travel as their `label()` plus their carried data: `TaskLifecycle::Done(reason)` and
14    /// `Residency::External { .. }` are semantic-kernel shapes, and mirroring them would make the
15    /// checkpoint a checkpoint of a private layout.
16    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    /// §5q-2 · one stored message, projected.
320    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    /// §7.10 · inline or by reference, decided by the message's own residency.
343    ///
344    /// The rule is not "is this body big" but "where does this body live": a result whose handle
345    /// says `External` was never resident in the first place (only its preview is in the message),
346    /// and one that says `PagedOut` left under pressure and lives with the host now. Either way the
347    /// checkpoint carries the reference and the digest that verifies a page-in — putting the bytes
348    /// back would re-create exactly the round trip §7.10 exists to delete.
349    pub(super) fn project_body(&self, message: &Message) -> StoredMessageBody {
350        // External/paged-out tool results must retain their reference form. Their text projection
351        // is represented as a `DurableToolResult`, but choosing that form first
352        // would discard the handle digest and make the body unreachable after restore.
353        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    // ----- §12.2 · the logical-state restore -----
443
444    /// Rebuild a driver from a checkpoint's logical state (§12.2 line 3).
445    ///
446    /// The exact inverse of [`Self::project_logical_state`], and deliberately nothing more: every
447    /// value written here is a value the projection reads back, so "did the restore work" is not a
448    /// judgement call — [`super::super::restore::restore_operation`] re-projects immediately afterwards and
449    /// compares the digest. A field this function forgets therefore fails the restore rather than
450    /// producing a runtime that is quietly one field short of the one that crashed.
451    ///
452    /// Task 16b makes every scheduler branch invertible here: workflow source nodes rebuild their
453    /// private graph indexes, queued signals rebuild priority and dedupe state, and child process
454    /// identity is restored without re-running permission defaults. Unknown labels and inconsistent
455    /// relationships still fail closed as `CheckpointIncompatible`.
456    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        // The engine is built from the configuration and then *moved* onto the checkpointed facts;
468        // it is never deserialised. Boot-only axes come from the genesis configuration the record
469        // froze, live-mutable ones from the patched configuration the checkpoint carries.
470        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}