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            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    /// §5q-2 · one stored message, projected.
363    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    /// §7.10 · inline or by reference, decided by the message's own residency.
387    ///
388    /// The rule is not "is this body big" but "where does this body live": a result whose handle
389    /// says `External` was never resident in the first place (only its preview is in the message),
390    /// and one that says `PagedOut` left under pressure and lives with the host now. Either way the
391    /// checkpoint carries the reference and the digest that verifies a page-in — putting the bytes
392    /// back would re-create exactly the round trip §7.10 exists to delete.
393    pub(super) fn project_body(&self, message: &CoreMessage) -> StoredMessageBody {
394        // External/paged-out tool results must retain their reference form. Their text projection
395        // is represented as a `DurableToolResult`, but choosing that form first
396        // would discard the handle digest and make the body unreachable after restore.
397        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    // ----- §12.2 · the logical-state restore -----
487
488    /// Rebuild a driver from a checkpoint's logical state (§12.2 line 3).
489    ///
490    /// The exact inverse of [`Self::project_logical_state`], and deliberately nothing more: every
491    /// value written here is a value the projection reads back, so "did the restore work" is not a
492    /// judgement call — [`super::super::restore::restore_operation`] re-projects immediately afterwards and
493    /// compares the digest. A field this function forgets therefore fails the restore rather than
494    /// producing a runtime that is quietly one field short of the one that crashed.
495    ///
496    /// Task 16b makes every scheduler branch invertible here: workflow source nodes rebuild their
497    /// private graph indexes, queued signals rebuild priority and dedupe state, and child process
498    /// identity is restored without re-running permission defaults. Unknown labels and inconsistent
499    /// relationships still fail closed as `CheckpointIncompatible`.
500    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        // The engine is built from the configuration and then *moved* onto the checkpointed facts;
512        // it is never deserialised. Boot-only axes come from the genesis configuration the record
513        // froze, live-mutable ones from the patched configuration the checkpoint carries.
514        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}