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                .enumerate()
298                .map(|(index, message)| {
299                    self.project_message(
300                        MessagePartition::System,
301                        message,
302                        ctx.partitions.system.measured_tokens(index, &ctx.engine),
303                    )
304                })
305                .chain(ctx.partitions.history.messages.iter().enumerate().map(
306                    |(index, message)| {
307                        self.project_message(
308                            MessagePartition::History,
309                            message,
310                            ctx.partitions.history.measured_tokens(index, &ctx.engine),
311                        )
312                    },
313                ))
314                .collect(),
315            task_state: project_task_state(&ctx.partitions.task_state),
316            partition_tokens: PartitionTokenState {
317                system: ctx.partitions.system.token_count,
318                knowledge: ctx.partitions.knowledge.token_count,
319                history: ctx.partitions.history.token_count,
320            },
321            history_len: ctx.partitions.history.messages.len() as u32,
322            frozen_history_len: ctx.frozen_history_len() as u32,
323            last_activity_ms: WireU64::new(ctx.last_activity_ms),
324            last_compact_ms: ctx.last_compact_ms.map(WireU64::new),
325        }
326    }
327
328    /// §5q-2 · one stored message, projected.
329    pub(super) fn project_message(
330        &self,
331        partition: MessagePartition,
332        message: &CoreMessage,
333        tokens: u32,
334    ) -> StoredMessageState {
335        StoredMessageState {
336            partition,
337            role: role_label(message.role).to_string(),
338            body: self.project_body(message),
339            tool_calls: message
340                .tool_calls
341                .iter()
342                .map(|call| LogicalToolCall {
343                    call_id: call.id.to_string(),
344                    name: call.name.to_string(),
345                    arguments: call.arguments.to_string(),
346                })
347                .collect(),
348            tokens,
349        }
350    }
351
352    /// §7.10 · inline or by reference, decided by the message's own residency.
353    ///
354    /// The rule is not "is this body big" but "where does this body live": a result whose handle
355    /// says `External` was never resident in the first place (only its preview is in the message),
356    /// and one that says `PagedOut` left under pressure and lives with the host now. Either way the
357    /// checkpoint carries the reference and the digest that verifies a page-in — putting the bytes
358    /// back would re-create exactly the round trip §7.10 exists to delete.
359    pub(super) fn project_body(&self, message: &CoreMessage) -> StoredMessageBody {
360        // External/paged-out tool results must retain their reference form. Their text projection
361        // is represented as a `DurableToolResult`, but choosing that form first
362        // would discard the handle digest and make the body unreachable after restore.
363        let single_tool_result_is_external =
364            match &message.content {
365                Content::Parts(parts) if parts.len() == 1 => match &parts[0] {
366                    ContentPart::ToolResult { call_id, .. } => {
367                        self.engine
368                            .as_ref()
369                            .and_then(|engine| {
370                                engine.ctx.handles.all().iter().find(|handle| {
371                                    handle.source.as_deref() == Some(call_id.as_str())
372                                })
373                            })
374                            .is_some_and(|handle| handle.residency.digest().is_some())
375                    }
376                    _ => false,
377                },
378                _ => false,
379            };
380        if !single_tool_result_is_external {
381            if let Some(results) = durable_tool_results_from_content(&message.content) {
382                return StoredMessageBody::Structured(StructuredMessageBody {
383                    durable_content: None,
384                    durable_tool_results: results,
385                });
386            }
387            if let Some(result) = durable_tool_result_from_content(&message.content) {
388                return StoredMessageBody::Structured(StructuredMessageBody {
389                    durable_content: None,
390                    durable_tool_results: vec![result],
391                });
392            }
393        }
394        let Some((text, tool_call_id, is_error)) = message_body_parts(message) else {
395            let durable_content = content_to_durable(&message.content)
396                .expect("every non-tool message uses the canonical durable content model");
397            return StoredMessageBody::Structured(StructuredMessageBody {
398                durable_content: Some(durable_content),
399                durable_tool_results: Vec::new(),
400            });
401        };
402        let Some(call_id) = tool_call_id.as_deref() else {
403            return StoredMessageBody::Inline(InlineMessageBody {
404                text,
405                tool_call_id,
406                is_error,
407            });
408        };
409        let referenced = self.engine.as_ref().and_then(|engine| {
410            let handle = engine
411                .ctx
412                .handles
413                .all()
414                .iter()
415                .find(|handle| handle.source.as_deref() == Some(call_id))?;
416            let digest = handle.residency.digest()?;
417            Some((
418                handle.id,
419                digest.to_string(),
420                matches!(handle.residency, Residency::PagedOut { .. }),
421            ))
422        });
423        match referenced {
424            Some((handle_id, digest, is_paged_out)) => {
425                StoredMessageBody::Reference(ReferencedMessageBody {
426                    handle_id,
427                    digest,
428                    preview: if is_paged_out {
429                        crate::context::renderer::collapse_preview(&text, call_id)
430                    } else {
431                        truncate_on_char_boundary(&text, self.preview_bytes())
432                    },
433                    tool_call_id,
434                    is_error,
435                })
436            }
437            None => StoredMessageBody::Inline(InlineMessageBody {
438                text,
439                tool_call_id,
440                is_error,
441            }),
442        }
443    }
444
445    pub(super) fn preview_bytes(&self) -> usize {
446        self.policy
447            .as_ref()
448            .map(|policy| policy.config().payload_policy.preview_bytes as usize)
449            .unwrap_or(2 * 1024)
450    }
451
452    // ----- §12.2 · the logical-state restore -----
453
454    /// Rebuild a driver from a checkpoint's logical state (§12.2 line 3).
455    ///
456    /// The exact inverse of [`Self::project_logical_state`], and deliberately nothing more: every
457    /// value written here is a value the projection reads back, so "did the restore work" is not a
458    /// judgement call — [`super::super::restore::restore_operation`] re-projects immediately afterwards and
459    /// compares the digest. A field this function forgets therefore fails the restore rather than
460    /// producing a runtime that is quietly one field short of the one that crashed.
461    ///
462    /// Task 16b makes every scheduler branch invertible here: workflow source nodes rebuild their
463    /// private graph indexes, queued signals rebuild priority and dedupe state, and child process
464    /// identity is restored without re-running permission defaults. Unknown labels and inconsistent
465    /// relationships still fail closed as `CheckpointIncompatible`.
466    pub fn restore_logical_state(
467        genesis_config: &ResolvedOperationConfig,
468        state: &LogicalKernelState,
469    ) -> Result<Self, KernelFault> {
470        let mut driver = Self::new();
471        let live_config = state
472            .syscall
473            .live_config
474            .clone()
475            .unwrap_or_else(|| genesis_config.clone());
476
477        // The engine is built from the configuration and then *moved* onto the checkpointed facts;
478        // it is never deserialised. Boot-only axes come from the genesis configuration the record
479        // froze, live-mutable ones from the patched configuration the checkpoint carries.
480        let mut engine = build_engine(genesis_config);
481        install_live_policies(&mut engine, &live_config);
482        engine.set_root_workflow(state.transition.root_kind == Some(RootKind::Workflow));
483        driver.policy = Some(LivePolicyState::restore(
484            state.syscall.policy_revision.unwrap_or(WireU64::ZERO),
485            live_config.clone(),
486        ));
487
488        restore_scheduler(&mut engine, &live_config, &state.scheduler)?;
489        if let Some(preempt) =
490            state
491                .transition
492                .pending_effects
493                .iter()
494                .find_map(|effect| match &effect.effect {
495                    EffectKind::PreemptTasks(preempt) => Some(preempt),
496                    _ => None,
497                })
498        {
499            engine.restore_pending_preempt(
500                preempt
501                    .attempts
502                    .iter()
503                    .map(|attempt| attempt.task_id.as_str().to_string())
504                    .collect(),
505                preempt.reason.clone(),
506            );
507        }
508        restore_context_vm(&mut engine, &state.context_vm)?;
509        engine.restore_memory_write_window(
510            state
511                .syscall
512                .memory_write_window_ms
513                .iter()
514                .map(|at| at.get())
515                .collect(),
516        );
517
518        if let Some(milestone) = &state.scheduler.milestone {
519            let contract = live_config
520                .verification_contract(&milestone.contract_id)
521                .ok_or_else(|| {
522                    KernelFault::new(
523                        KernelFaultCode::CheckpointIncompatible,
524                        format!(
525                            "the checkpoint runs verification contract {:?}, which this \
526                             operation's configuration no longer declares",
527                            milestone.contract_id
528                        ),
529                    )
530                })?;
531            engine.load_milestone_contract(core_milestone_contract(contract, &live_config));
532            if !engine
533                .restore_milestone_cursor(milestone.phase_id.as_deref(), milestone.blocked_count)
534            {
535                return Err(KernelFault::new(
536                    KernelFaultCode::CheckpointIncompatible,
537                    format!(
538                        "the checkpoint sits on milestone phase {:?} of contract {:?}, which that \
539                         contract does not declare",
540                        milestone.phase_id, milestone.contract_id
541                    ),
542                ));
543            }
544            driver.loaded_contract_id = Some(milestone.contract_id.clone());
545        }
546
547        driver.engine = Some(engine);
548        driver.root_kind = state.transition.root_kind;
549        driver.focus = state.transition.focus.clone();
550        if let Some(workflow) = &state.scheduler.workflow {
551            driver.workflow_id = Some(workflow.workflow_id.clone());
552            driver.node_ids = workflow
553                .nodes
554                .iter()
555                .map(|node| node.node_id.clone())
556                .collect();
557            driver.workflow_nodes = workflow
558                .nodes
559                .iter()
560                .map(|node| WireNode {
561                    node_id: node.node_id.clone(),
562                    task: node.task.clone(),
563                    depends_on: node.depends_on.clone(),
564                    run_spec: node.run_spec.clone(),
565                })
566                .collect();
567        }
568        driver.attempts = state
569            .scheduler
570            .attempts
571            .iter()
572            .map(|attempt| {
573                (
574                    attempt.task_id.as_str().to_string(),
575                    attempt.attempt_id.clone(),
576                )
577            })
578            .collect();
579        driver.provider_calls = state
580            .syscall
581            .provider_calls
582            .iter()
583            .map(|call| {
584                (
585                    call.effect_id.clone(),
586                    PendingProviderCall {
587                        task_id: call.task_id.clone(),
588                        exposed_tools: call.exposed_tools.iter().cloned().collect(),
589                    },
590                )
591            })
592            .collect();
593        driver.consumed_calls = state.syscall.consumed_call_ids.iter().cloned().collect();
594        driver.pending_memory_writes = state
595            .syscall
596            .authored_memory_writes
597            .iter()
598            .map(|write| {
599                (
600                    write.effect_id.clone(),
601                    AuthoredMemoryWrite {
602                        binding_id: write.binding_id.clone(),
603                        name: write.name.clone(),
604                        kind: write.kind,
605                        size_bytes: write.size_bytes,
606                    },
607                )
608            })
609            .collect();
610        driver.pending_memory_queries = state
611            .syscall
612            .authored_memory_queries
613            .iter()
614            .map(|query| {
615                (
616                    query.effect_id.clone(),
617                    AuthoredMemoryQuery {
618                        binding_id: query.binding_id.clone(),
619                        text: query.text.clone(),
620                        requested_k: query.requested_k,
621                    },
622                )
623            })
624            .collect();
625        driver.pending_payload_loads = state
626            .context_vm
627            .pending_payload_loads
628            .iter()
629            .map(|load| {
630                (
631                    load.effect_id.clone(),
632                    PendingPayloadLoad {
633                        handle_id: load.handle_id.clone(),
634                        digest: load.digest.clone(),
635                        original_size: load.original_size.map(WireU64::get),
636                    },
637                )
638            })
639            .collect();
640        Ok(driver)
641    }
642}