Skip to main content

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

1use super::*;
2
3impl CanonicalOperationDriver {
4    // ----- the plan function -----
5
6    /// Plan one input.
7    ///
8    /// Pass this to [`KernelTransaction::prepare`](super::super::transaction::KernelTransaction::prepare).
9    /// The focus/root-kind fold does **not** advance here — call [`Self::note_committed`] once the
10    /// host's append and the transaction's commit have both succeeded.
11    pub fn plan(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
12        if let Some(fault) = &self.poison {
13            return Err(fault.clone());
14        }
15        if let Some(staged) = &self.staged {
16            let staged_seq = staged.step_seq;
17            return Err(self.poison_with(KernelFault::new(
18                KernelFaultCode::TransactionConflict,
19                format!(
20                    "the driver still holds the plan of step {staged_seq}; its transition never \
21                     committed while the semantic kernel already advanced under it, so this \
22                     runtime no longer describes the journal — rebuild from the records"
23                ),
24            )));
25        }
26        let mut step = self.plan_inner(context)?;
27        step.observations = self
28            .engine
29            .as_mut()
30            .map(LoopStateMachine::take_observations)
31            .unwrap_or_default();
32        self.staged = Some(StagedFocus {
33            step_seq: context.step_seq,
34            root_kind: step.root_kind,
35            focus: step.focus.clone(),
36        });
37        Ok(step)
38    }
39
40    /// Install the staged fold after the transaction committed the record (§7.4: a focus moves only
41    /// on a committed transition).
42    pub fn note_committed(&mut self, step_seq: WireU64) -> Result<(), KernelFault> {
43        if let Some(fault) = &self.poison {
44            return Err(fault.clone());
45        }
46        let Some(staged) = self.staged.take() else {
47            return Err(self.poison_with(KernelFault::new(
48                KernelFaultCode::TransactionConflict,
49                format!("step {step_seq} committed, but the driver planned no such step"),
50            )));
51        };
52        if staged.step_seq != step_seq {
53            let planned = staged.step_seq;
54            return Err(self.poison_with(KernelFault::new(
55                KernelFaultCode::TransactionConflict,
56                format!("step {step_seq} committed, but the driver planned step {planned}"),
57            )));
58        }
59        if let Some(kind) = staged.root_kind {
60            self.root_kind = Some(kind);
61        }
62        self.focus = staged.focus;
63        Ok(())
64    }
65
66    /// Plan **and** fold in one call — the shape
67    /// [`rebuild_from_records`](super::super::transaction::KernelTransaction::rebuild_from_records) needs,
68    /// where every record it replays is by definition already durable.
69    pub fn fold(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
70        let step = self.plan(context)?;
71        self.note_committed(context.step_seq)?;
72        Ok(step)
73    }
74
75    // ----- §10.2 · the agent-authored workflow seam -----
76
77    /// Enter a workflow the agent asked for, inside an agent root (§10.2).
78    ///
79    /// This is the P1 reduction point Task 10 wires its `SyscallRequest::SubmitWorkflow` gate to;
80    /// the authority rules it enforces are already the final ones:
81    ///
82    /// * the root kind stays `Agent` — a syscall never re-roots an operation;
83    /// * the focus moves to `WorkflowController { parent_task_id: Some(agent task) }`;
84    /// * depth is at most 1. Asking for a workflow while the focus already *is* a
85    ///   `WorkflowController` is an `InvalidAuthority` fault with zero mutation — workflows do not
86    ///   stack (§15.4).
87    pub fn begin_nested_workflow(
88        &mut self,
89        context: &PlanContext<'_>,
90        spec: &WireSpec,
91    ) -> Result<PlannedStep, KernelFault> {
92        if let Some(fault) = &self.poison {
93            return Err(fault.clone());
94        }
95        let mut index = 0;
96        let outcome = self
97            .enter_nested_workflow(context, spec, &mut index)
98            .map_err(|refusal| match refusal {
99                SyscallRefusal::Fault(fault) => fault,
100                // A direct caller has no observation channel, so the gate's denial becomes the
101                // transition's refusal. Through the P1 path the same denial is an audit fact.
102                SyscallRefusal::Rejected(rejected) => {
103                    KernelFault::new(KernelFaultCode::ResourceLimitExceeded, rejected.reason)
104                }
105            })?;
106        let step = PlannedStep {
107            root_kind: Some(RootKind::Agent),
108            focus: outcome.focus,
109            observations: self
110                .engine
111                .as_mut()
112                .map(LoopStateMachine::take_observations)
113                .unwrap_or_default(),
114            disposition: StepDisposition::Effects(EffectsDisposition {
115                effects: outcome.effects,
116            }),
117        };
118        self.staged = Some(StagedFocus {
119            step_seq: context.step_seq,
120            root_kind: step.root_kind,
121            focus: step.focus.clone(),
122        });
123        Ok(step)
124    }
125
126    /// The body of [`Self::begin_nested_workflow`], without the staging — so a syscall batch that
127    /// also carries other requests composes it instead of racing it for the staging slot.
128    pub(super) fn enter_nested_workflow(
129        &mut self,
130        context: &PlanContext<'_>,
131        spec: &WireSpec,
132        effect_index: &mut u32,
133    ) -> Result<SyscallOutcome, SyscallRefusal> {
134        let staged = self.staged.as_ref().map(|staged| staged.focus.clone());
135        let focus = staged.as_ref().unwrap_or(&self.focus);
136        let root_kind = self.root_kind;
137
138        let parent_task_id = match (root_kind, focus) {
139            (Some(RootKind::Agent), Some(ExecutionFocus::AgentTurn(turn))) => turn.task_id.clone(),
140            (Some(RootKind::Agent), Some(ExecutionFocus::WorkflowController(_))) => {
141                return Err(authority(
142                    "a workflow is already the execution focus; workflows do not stack, so a \
143                     second start request is refused with no spawn effect (§7.4 focus depth ≤ 1)",
144                ));
145            }
146            (Some(RootKind::Workflow), _) => {
147                return Err(authority(
148                    "this operation's root is a workflow; its focus never moves, and a nested \
149                     workflow start is not a transition it admits (§7.4)",
150                ));
151            }
152            _ => {
153                return Err(SyscallRefusal::Fault(KernelFault::new(
154                    KernelFaultCode::InvalidLifecycle,
155                    "no root has started, so there is no agent turn to suspend".to_string(),
156                )));
157            }
158        };
159
160        for node in &spec.nodes {
161            self.require_known_contract(context.config, node.run_spec.as_ref())
162                .map_err(|fault| {
163                    SyscallRefusal::Rejected(SyscallRejection::new("start_workflow", fault.message))
164                })?;
165        }
166        let core_spec = build_core_spec(spec).map_err(SyscallRefusal::Fault)?;
167        let node_ids = wire_node_ids(spec);
168        let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
169        self.require_effect_support(context.config, EffectKindTag::SpawnTasks)
170            .map_err(SyscallRefusal::Fault)?;
171
172        // §10.2 · the resource gate runs before the DAG is installed, so a denial commits with no
173        // spawn effect at all rather than with a workflow the run cannot afford.
174        let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
175        let disposition = engine.gate_syscall(&CoreSyscall::LoadWorkflow {
176            node_count: spec.nodes.len(),
177        });
178        if !disposition.is_allowed() {
179            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
180                "start_workflow",
181                denial_reason(&disposition, "workflow authoring denied"),
182            )));
183        }
184
185        // ----- past this line the semantic engine advances -----
186        engine.set_root_workflow(false);
187        let action = engine.load_workflow_as(core_spec, parent_task_id.as_str());
188        self.node_ids = node_ids;
189        self.workflow_nodes = spec.nodes.clone();
190        self.workflow_id = Some(workflow_id.clone());
191        let disposition = self
192            .disposition_for_at(context, action, RootKind::Agent, effect_index)
193            .map_err(SyscallRefusal::Fault)?;
194        let StepDisposition::Effects(effects) = disposition else {
195            return Err(SyscallRefusal::Fault(KernelFault::new(
196                KernelFaultCode::InvalidLifecycle,
197                "entering a nested workflow cannot terminate the operation".to_string(),
198            )));
199        };
200        Ok(SyscallOutcome {
201            effects: effects.effects,
202            focus: Some(ExecutionFocus::workflow_controller(
203                workflow_id,
204                Some(parent_task_id),
205            )),
206            needs_workflow_round: false,
207            ack: None,
208        })
209    }
210
211    pub(super) fn mint_effect(
212        &self,
213        context: &PlanContext<'_>,
214        effect: EffectKind,
215        effect_index: &mut u32,
216    ) -> KernelEffect {
217        let effect_id =
218            mint_effect_id(&context.input.operation_id, context.step_seq, *effect_index);
219        *effect_index += 1;
220        KernelEffect {
221            effect_id,
222            causation_input_id: context.input.input_id.clone(),
223            effect,
224        }
225    }
226
227    /// Fold one engine action's effects into an accumulating step.
228    pub(super) fn extend_with_action(
229        &mut self,
230        context: &PlanContext<'_>,
231        action: LoopAction,
232        root_kind: RootKind,
233        effect_index: &mut u32,
234        effects: &mut Vec<KernelEffect>,
235    ) -> Result<(), KernelFault> {
236        match self.disposition_for_at(context, action, root_kind, effect_index)? {
237            StepDisposition::Effects(published) => {
238                effects.extend(published.effects);
239                Ok(())
240            }
241            StepDisposition::Terminal(_) => Err(KernelFault::new(
242                KernelFaultCode::InvalidLifecycle,
243                "a syscall batch cannot terminate the operation; §7.12 admits effects or a \
244                 terminal, never both in one step"
245                    .to_string(),
246            )),
247        }
248    }
249
250    // ----- internals -----
251
252    pub(super) fn plan_inner(
253        &mut self,
254        context: &PlanContext<'_>,
255    ) -> Result<PlannedStep, KernelFault> {
256        // Every transition reads the observations *its own* semantic call produced. `start`/`feed`
257        // clear the buffer themselves; `load_workflow` and `resolve_workflow_spawn` do not, so the
258        // driver clears it here rather than letting a stale `WorkflowCompleted` from an earlier
259        // step decide a later one's disposition.
260        if let Some(engine) = self.engine.as_mut() {
261            engine.take_observations();
262            // §11.2 · the envelope's accepted time is this operation's only clock, and it is fed
263            // once, here, before any semantic call. Every clock-dependent decision the step makes
264            // (signal TTL and deadline escalation, rate-limit windows, the wall-time budget axis)
265            // therefore reads a fact the journal already holds, so a replay decides identically.
266            engine.observe_accepted_time(context.input.observed_at_ms.get());
267            let woken = engine
268                .task_table_mut()
269                .wake_expired_timers(context.input.observed_at_ms.get());
270            if !woken.is_empty() {
271                engine.observe_local_runnable_tasks();
272            }
273        }
274        match &context.input.input {
275            NormalizedPayload::ConfigureOperation(configure) => {
276                self.plan_configure(&configure.config)
277            }
278            NormalizedPayload::StartOperation(start) => {
279                self.plan_start(context, &start.entry, &start.initial_context)
280            }
281            NormalizedPayload::ResolveEffect(resolve) => self.plan_resolve_effect(context, resolve),
282            NormalizedPayload::DeliverExternalEvent(event) => {
283                self.plan_external_event(context, &event.event)
284            }
285            NormalizedPayload::HostControl(control) => {
286                self.plan_host_control(context, &control.command)
287            }
288        }
289    }
290
291    /// §6.1.2 · genesis. The engine is built from the **resolved** configuration the record froze,
292    /// never from this binary's defaults, so a rebuild on a newer kernel plans the same step.
293    pub(super) fn plan_configure(
294        &mut self,
295        config: &ResolvedOperationConfig,
296    ) -> Result<PlannedStep, KernelFault> {
297        self.engine = Some(build_engine(config));
298        // §13.2 · the live-mutable half starts at revision 0, holding exactly what the genesis
299        // record froze. A patch rebases onto this, never onto a compile-time default.
300        self.policy = Some(LivePolicyState::new(config.clone()));
301        Ok(PlannedStep::quiet(None, None))
302    }
303
304    /// §7.4 · the one atomic root start.
305    ///
306    /// Both arms are ordered the same way and for the same reason: every refusal this transition
307    /// can raise is decided while nothing has moved, and only then does the semantic engine
308    /// advance. A rejected root start therefore leaves an operation that is still `Configured` and
309    /// still free to choose a root.
310    pub(super) fn plan_start(
311        &mut self,
312        context: &PlanContext<'_>,
313        entry: &RootEntry,
314        initial: &InitialContext,
315    ) -> Result<PlannedStep, KernelFault> {
316        if self.root_kind.is_some() || self.staged.is_some() {
317            return Err(KernelFault::new(
318                KernelFaultCode::InvalidLifecycle,
319                "this operation already has a root; a root entry is chosen once and is immutable \
320                 (§6.1.3–6.1.5)"
321                    .to_string(),
322            ));
323        }
324
325        match entry {
326            RootEntry::Agent(agent) => {
327                self.require_effect_support(context.config, EffectKindTag::CallProvider)?;
328                self.require_known_contract(context.config, agent.run_spec.as_ref())?;
329                let task = runtime_task(&agent.task);
330                let run_spec = agent.run_spec.as_ref().map(agent_run_spec);
331
332                // ----- past this line the semantic engine advances -----
333                // The cascade is installed before `start`, which is the engine's own precondition:
334                // a contract loaded afterwards would leave phase 0 already behind the run.
335                self.load_verification_contract(context.config, agent.run_spec.as_ref())?;
336                let engine = self.engine_mut()?;
337                seed_initial_context(engine, initial);
338                engine.run_spec = run_spec;
339                let action = engine.start(task);
340                let disposition = self.disposition_for(context, action, RootKind::Agent)?;
341                if !publishes(&disposition, EffectKindTag::CallProvider) {
342                    return Err(KernelFault::new(
343                        KernelFaultCode::InvalidLifecycle,
344                        "an agent root's first committed step must publish a provider call (§7.4)"
345                            .to_string(),
346                    ));
347                }
348                Ok(PlannedStep {
349                    root_kind: Some(RootKind::Agent),
350                    focus: Some(ExecutionFocus::agent_turn(root_task_id())),
351                    observations: Vec::new(),
352                    disposition,
353                })
354            }
355            RootEntry::Workflow(workflow) => {
356                self.require_effect_support(context.config, EffectKindTag::SpawnTasks)?;
357                for node in &workflow.spec.nodes {
358                    self.require_known_contract(context.config, node.run_spec.as_ref())?;
359                }
360                if workflow.spec.nodes.is_empty() {
361                    return Err(KernelFault::new(
362                        KernelFaultCode::InvalidConfig,
363                        "a workflow root with no nodes has no first task to spawn; a root entry \
364                         must be able to publish its first effect (§10.1)"
365                            .to_string(),
366                    ));
367                }
368                let core_spec = build_core_spec(&workflow.spec)?;
369                let node_ids = wire_node_ids(&workflow.spec);
370                let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
371
372                // ----- past this line the semantic engine advances -----
373                let engine = self.engine_mut()?;
374                seed_initial_context(engine, initial);
375                // §6.1.7 — this DAG *is* the root, so its completion is the operation's terminal
376                // rather than one more turn of a parent agent loop.
377                engine.set_root_workflow(true);
378                let action = engine.load_workflow_as(core_spec, ROOT_TASK_ID);
379                self.node_ids = node_ids;
380                self.workflow_nodes = workflow.spec.nodes.clone();
381                self.workflow_id = Some(workflow_id.clone());
382                let disposition = self.disposition_for(context, action, RootKind::Workflow)?;
383                if !publishes(&disposition, EffectKindTag::SpawnTasks) {
384                    return Err(KernelFault::new(
385                        KernelFaultCode::InvalidLifecycle,
386                        "a workflow root's first committed step must publish a task spawn, never a \
387                         provider call (§10.1)"
388                            .to_string(),
389                    ));
390                }
391                Ok(PlannedStep {
392                    root_kind: Some(RootKind::Workflow),
393                    focus: Some(ExecutionFocus::workflow_controller(workflow_id, None)),
394                    observations: Vec::new(),
395                    disposition,
396                })
397            }
398        }
399    }
400}