Skip to main content

deepstrike_core/runtime/kernel/wire/
root.rs

1//! Root entry, execution focus and the logical payloads a root start carries (spec §7.4).
2
3use serde::{Deserialize, Serialize};
4
5use super::scalar::{BoundedJson, CallId, NodeId, TaskId, WireU64, WorkflowId};
6
7// ---------------------------------------------------------------------------------------------
8// root entry
9// ---------------------------------------------------------------------------------------------
10
11/// The **only** way an operation starts (§7.4). There is no second start shape: no generic
12/// `Resume`, no root `LoadWorkflow` that first pretends to be an agent run, no host-minted
13/// sub-agent spawn.
14///
15/// * an agent root goes straight to a provider call;
16/// * a workflow root builds its DAG and spawns tasks directly, and its completion commits the
17///   root terminal itself — nothing external "completes the run".
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20pub enum RootEntry {
21    Agent(RootAgentEntry),
22    Workflow(RootWorkflowEntry),
23}
24
25impl RootEntry {
26    pub fn root_kind(&self) -> RootKind {
27        match self {
28            Self::Agent(_) => RootKind::Agent,
29            Self::Workflow(_) => RootKind::Workflow,
30        }
31    }
32}
33
34/// Agent root. `initial_context` is deliberately absent: it belongs to `StartOperation` and is
35/// never duplicated per variant.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct RootAgentEntry {
39    pub task: LogicalTask,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub run_spec: Option<LogicalAgentSpec>,
42}
43
44/// Workflow root.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct RootWorkflowEntry {
48    pub spec: WorkflowSpec,
49}
50
51/// Immutable for the whole operation lifetime (§6.1.5/§6.1.6).
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum RootKind {
55    Agent,
56    Workflow,
57}
58
59/// Where the operation's control flow currently is.
60///
61/// GAP-1 fixes a **closed** transition table; the transitions themselves are implemented with the
62/// root execution work (Phase 3), and this type is what they are allowed to express:
63///
64/// * `RootKind::Agent` starts at `AgentTurn { root task }`. Exactly two transitions exist:
65///   when the workflow an agent started through a P1 syscall **commits** its start effect, focus
66///   moves to `WorkflowController { workflow_id, parent_task_id: Some(agent task) }`; when that
67///   workflow completes (success, failure or cancellation) and the completion **commits**, focus
68///   moves back to the original `AgentTurn`. Depth is at most 1 — workflows have no stack
69///   (§10.2), so requesting another workflow while the focus is a `WorkflowController` is an
70///   `InvalidAuthority` fault.
71/// * `RootKind::Workflow` is permanently `WorkflowController { root workflow, parent_task_id:
72///   None }`. Agent execution inside a DAG node is a P2 child attempt and does not move the
73///   root's focus.
74/// * Focus only ever moves on a **committed** transition. There is no input — host command or
75///   otherwise — that sets it directly.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(tag = "kind", rename_all = "snake_case")]
78pub enum ExecutionFocus {
79    AgentTurn(AgentTurnFocus),
80    WorkflowController(WorkflowControllerFocus),
81}
82
83/// Newtype payloads rather than inline variants: `deny_unknown_fields` does not apply to an
84/// inline struct variant of an internally tagged enum.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct AgentTurnFocus {
88    pub task_id: TaskId,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct WorkflowControllerFocus {
94    pub workflow_id: WorkflowId,
95    /// `Some` ⇒ a nested workflow inside an agent root; its completion restores the parent agent
96    /// instead of committing a root terminal. `None` ⇒ the root workflow itself.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub parent_task_id: Option<TaskId>,
99}
100
101impl ExecutionFocus {
102    pub fn agent_turn(task_id: TaskId) -> Self {
103        Self::AgentTurn(AgentTurnFocus { task_id })
104    }
105
106    pub fn workflow_controller(workflow_id: WorkflowId, parent_task_id: Option<TaskId>) -> Self {
107        Self::WorkflowController(WorkflowControllerFocus {
108            workflow_id,
109            parent_task_id,
110        })
111    }
112
113    /// The root kind this focus is only ever reachable from. A `WorkflowController` with a parent
114    /// task is the nested case of an `Agent` root, so callers that need to distinguish "nested"
115    /// from "root workflow" use [`Self::is_nested_in_agent`].
116    pub fn root_kind_hint(&self) -> RootKind {
117        match self {
118            Self::AgentTurn(_) => RootKind::Agent,
119            Self::WorkflowController(_) => RootKind::Workflow,
120        }
121    }
122
123    /// Whether this focus is the nested workflow of an agent root — the only case in which a
124    /// workflow completion must restore a parent agent rather than terminate the operation.
125    pub fn is_nested_in_agent(&self) -> bool {
126        matches!(
127            self,
128            Self::WorkflowController(WorkflowControllerFocus {
129                parent_task_id: Some(_),
130                ..
131            })
132        )
133    }
134}
135
136// ---------------------------------------------------------------------------------------------
137// logical payloads
138// ---------------------------------------------------------------------------------------------
139
140/// What the operation is trying to achieve. Purely logical: no session, no path, no host handle.
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct LogicalTask {
144    pub goal: String,
145    #[serde(default, skip_serializing_if = "Vec::is_empty")]
146    pub criteria: Vec<String>,
147    /// Free-form host label, carried through untouched — the kernel attaches no scheduling
148    /// semantics to it.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub lane: Option<String>,
151    #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
152    pub metadata: BoundedJson,
153}
154
155impl LogicalTask {
156    pub fn new(goal: impl Into<String>) -> Self {
157        Self {
158            goal: goal.into(),
159            criteria: Vec::new(),
160            lane: None,
161            metadata: BoundedJson::null(),
162        }
163    }
164}
165
166/// How the root agent should run — a **wire DTO of its own**, not the SDK's `AgentRunSpec`.
167///
168/// The SDK spec carries an `AgentIdentity` with `session_id`/`parent_session_id`: host storage
169/// identity that the kernel must never learn, must never persist in a record, and must never be
170/// able to correlate across operations. The logical spec carries only what changes kernel
171/// decisions.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct LogicalAgentSpec {
175    pub goal: String,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub role: Option<AgentRole>,
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub isolation: Option<AgentIsolation>,
180    /// Logical context carried into a workflow child. This is operation-local content selection,
181    /// not a host session reference, so it is safe to persist and replay in canonical records.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub context_inheritance: Option<LogicalContextInheritance>,
184    /// Logical id of a verification contract in the operation's catalog.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub verification_contract_id: Option<String>,
187    #[serde(default, skip_serializing_if = "CapabilityFilter::is_empty")]
188    pub capability_filter: CapabilityFilter,
189    /// Pre-activation tool surface *under* the capability ceiling. `None` ⇒ the ceiling itself;
190    /// `Some([])` is a legitimate, distinct minimal surface.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub exposure_baseline: Option<Vec<String>>,
193    /// Pure logical pacing policy for one loop round. It contains no host/session identity.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub loop_round: Option<LogicalLoopRoundSpec>,
196    #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
197    pub metadata: BoundedJson,
198}
199
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[serde(deny_unknown_fields)]
202pub struct LogicalLoopRoundSpec {
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub max_rounds: Option<u32>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub min_sleep_ms: Option<WireU64>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub max_sleep_ms: Option<WireU64>,
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub default_action: Option<String>,
211}
212
213impl LogicalAgentSpec {
214    pub fn new(goal: impl Into<String>) -> Self {
215        Self {
216            goal: goal.into(),
217            role: None,
218            isolation: None,
219            context_inheritance: None,
220            verification_contract_id: None,
221            capability_filter: CapabilityFilter::default(),
222            exposure_baseline: None,
223            loop_round: None,
224            metadata: BoundedJson::null(),
225        }
226    }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum AgentRole {
232    Explore,
233    Plan,
234    Implement,
235    Verify,
236    Custom,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "snake_case")]
241pub enum AgentIsolation {
242    Shared,
243    ReadOnly,
244    Worktree,
245    Remote,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub enum LogicalContextInheritance {
251    None,
252    SystemOnly,
253    Full,
254}
255
256/// Capability ceiling for a run. Empty on an axis ⇒ that axis does not narrow anything.
257#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259pub struct CapabilityFilter {
260    #[serde(default, skip_serializing_if = "Vec::is_empty")]
261    pub allowed_kinds: Vec<CapabilityKind>,
262    #[serde(default, skip_serializing_if = "Vec::is_empty")]
263    pub allowed_ids: Vec<String>,
264}
265
266impl CapabilityFilter {
267    pub fn is_empty(&self) -> bool {
268        self.allowed_kinds.is_empty() && self.allowed_ids.is_empty()
269    }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274pub enum CapabilityKind {
275    Tool,
276    Skill,
277    Memory,
278    Knowledge,
279    McpServer,
280    Command,
281    Agent,
282}
283
284/// A workflow DAG as the host or an agent declares it.
285///
286/// The node/edge vocabulary converges with the root-execution and dynamic-append work (Task 9 /
287/// Task 10). What Task 3 fixes is that a workflow root enters through [`RootEntry::Workflow`]
288/// and carries no host-authored recovery state, because recovery is the kernel checkpoint's job
289/// (§12.4).
290#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
291#[serde(deny_unknown_fields)]
292pub struct WorkflowSpec {
293    #[serde(default, skip_serializing_if = "String::is_empty")]
294    pub name: String,
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub nodes: Vec<WorkflowNode>,
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300#[serde(deny_unknown_fields)]
301pub struct WorkflowNode {
302    pub node_id: NodeId,
303    pub task: LogicalTask,
304    #[serde(default, skip_serializing_if = "Vec::is_empty")]
305    pub depends_on: Vec<NodeId>,
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub run_spec: Option<LogicalAgentSpec>,
308}
309
310/// What the kernel needs to start thinking, and nothing else (§7.4).
311///
312/// Explicitly **not** here: session logs, provider replay envelopes, host paths. Those are host
313/// storage concerns; a kernel that accepted them would be persisting facts it cannot reproduce.
314#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
315#[serde(deny_unknown_fields)]
316pub struct InitialContext {
317    #[serde(default, skip_serializing_if = "Vec::is_empty")]
318    pub messages: Vec<LogicalMessage>,
319    #[serde(default, skip_serializing_if = "Vec::is_empty")]
320    pub knowledge: Vec<KnowledgeEntry>,
321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
322    pub capabilities: Vec<CapabilityGrant>,
323}
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326#[serde(deny_unknown_fields)]
327pub struct LogicalMessage {
328    pub role: MessageRole,
329    pub content: String,
330    /// Host-observed token count. Absent ⇒ the kernel estimates.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub tokens: Option<u32>,
333    /// Set on a tool message to pair it with its call.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub tool_call_id: Option<CallId>,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum MessageRole {
341    System,
342    User,
343    Assistant,
344    Tool,
345}
346
347/// One knowledge-partition entry. `key` gives it identity (upsert semantics); `pinned` exempts it
348/// from the knowledge budget sweep.
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350#[serde(deny_unknown_fields)]
351pub struct KnowledgeEntry {
352    pub content: String,
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub key: Option<String>,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub tokens: Option<u32>,
357    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
358    pub pinned: bool,
359}
360
361/// A capability the operation may use, by logical identity.
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct CapabilityGrant {
365    pub kind: CapabilityKind,
366    pub id: String,
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub description: Option<String>,
369}
370
371/// A capability reference used when withdrawing a grant.
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
373#[serde(deny_unknown_fields)]
374pub struct CapabilityRef {
375    pub kind: CapabilityKind,
376    pub id: String,
377}