@deepstrike/wasm 0.2.35 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
2
- import type { KernelEventCategory, KernelPrimitive } from "./kernel-event-log.js";
2
+ import type { KernelPrimitive } from "./kernel-event-log.js";
3
3
  export type RollbackReason = {
4
4
  kind: "fatal_tool_error";
5
5
  tool_name: string;
@@ -74,8 +74,6 @@ export type SessionEvent = {
74
74
  } | {
75
75
  kind: "compressed";
76
76
  turn: number;
77
- category?: KernelEventCategory;
78
- primitive?: KernelPrimitive;
79
77
  archived_seq_range: [number, number];
80
78
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
81
79
  summary?: string;
@@ -85,8 +83,6 @@ export type SessionEvent = {
85
83
  } | {
86
84
  kind: "page_out";
87
85
  turn: number;
88
- category?: KernelEventCategory;
89
- primitive?: KernelPrimitive;
90
86
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
91
87
  summary?: string;
92
88
  tier_hint?: string;
@@ -94,14 +90,10 @@ export type SessionEvent = {
94
90
  } | {
95
91
  kind: "page_in";
96
92
  turn: number;
97
- category?: KernelEventCategory;
98
- primitive?: KernelPrimitive;
99
93
  entry_count: number;
100
94
  } | {
101
95
  kind: "large_result_spooled";
102
96
  turn: number;
103
- category?: KernelEventCategory;
104
- primitive?: KernelPrimitive;
105
97
  call_id: string;
106
98
  tool: string;
107
99
  original_size: number;
@@ -110,15 +102,11 @@ export type SessionEvent = {
110
102
  } | {
111
103
  kind: "rollbacked";
112
104
  turn: number;
113
- category?: KernelEventCategory;
114
- primitive?: KernelPrimitive;
115
105
  checkpoint_history_len: number;
116
106
  reason?: RollbackReason;
117
107
  } | {
118
108
  kind: "capability_changed";
119
109
  turn: number;
120
- category?: KernelEventCategory;
121
- primitive?: KernelPrimitive;
122
110
  added: string[];
123
111
  removed: string[];
124
112
  change_kind?: string;
@@ -129,78 +117,51 @@ export type SessionEvent = {
129
117
  } | {
130
118
  kind: "context_renewed";
131
119
  turn: number;
132
- category?: KernelEventCategory;
133
- primitive?: KernelPrimitive;
134
120
  sprint: number;
135
121
  handoff_ref: string;
136
122
  } | {
137
123
  kind: "suspended";
138
124
  turn: number;
139
- category?: KernelEventCategory;
140
- primitive?: KernelPrimitive;
141
125
  reason: string;
142
126
  pending_calls?: string[];
143
127
  } | {
144
128
  kind: "resumed";
145
129
  turn: number;
146
- category?: KernelEventCategory;
147
- primitive?: KernelPrimitive;
148
130
  approved?: string[];
149
131
  denied?: string[];
150
132
  } | {
151
133
  kind: "tool_gated";
152
134
  turn: number;
153
- category?: KernelEventCategory;
154
- primitive?: KernelPrimitive;
155
135
  call_id: string;
156
136
  tool: string;
157
137
  reason: string;
158
138
  } | {
159
139
  kind: "signal_disposed";
160
140
  turn: number;
161
- category?: KernelEventCategory;
162
- primitive?: KernelPrimitive;
163
141
  signal_id: string;
164
142
  disposition: string;
165
143
  queue_depth: number;
166
144
  } | {
167
145
  kind: "budget_exceeded";
168
146
  turn: number;
169
- category?: KernelEventCategory;
170
- primitive?: KernelPrimitive;
171
147
  budget: string;
172
148
  } | {
173
149
  kind: "milestone_advanced";
174
150
  turn: number;
175
- category?: KernelEventCategory;
176
- primitive?: KernelPrimitive;
177
151
  phase_id: string;
178
152
  capabilities_unlocked: string[];
179
153
  } | {
180
154
  kind: "milestone_blocked";
181
155
  turn: number;
182
- category?: KernelEventCategory;
183
- primitive?: KernelPrimitive;
184
156
  phase_id: string;
185
157
  reason: string;
186
- } | {
187
- kind: "milestone_evidence";
188
- turn: number;
189
- category?: KernelEventCategory;
190
- primitive?: KernelPrimitive;
191
- phase_id: string;
192
- evidence: string[];
193
158
  } | {
194
159
  kind: "checkpoint_taken";
195
160
  turn: number;
196
- category?: KernelEventCategory;
197
- primitive?: KernelPrimitive;
198
161
  history_len: number;
199
162
  } | {
200
163
  kind: "agent_process_changed";
201
164
  turn: number;
202
- category?: KernelEventCategory;
203
- primitive?: KernelPrimitive;
204
165
  agent_id: string;
205
166
  parent_session_id: string;
206
167
  role: string;
@@ -212,52 +173,52 @@ export type SessionEvent = {
212
173
  } | {
213
174
  kind: "memory_written";
214
175
  turn: number;
215
- category?: KernelEventCategory;
216
- primitive?: KernelPrimitive;
217
176
  memory_id: string;
218
177
  memory_kind: string;
219
178
  size_bytes: number;
220
179
  } | {
221
180
  kind: "memory_queried";
222
181
  turn: number;
223
- category?: KernelEventCategory;
224
- primitive?: KernelPrimitive;
225
182
  query_context: string;
226
183
  requested_k: number;
227
184
  requires_async_response: boolean;
228
185
  } | {
229
186
  kind: "memory_validation_failed";
230
187
  turn: number;
231
- category?: KernelEventCategory;
232
- primitive?: KernelPrimitive;
233
188
  memory_id: string;
234
189
  error: string;
235
190
  } | {
236
191
  kind: "workflow_node_completed";
237
192
  turn: number;
238
- category?: KernelEventCategory;
239
- primitive?: KernelPrimitive;
240
193
  agent_id: string;
241
194
  termination: string;
195
+ /** W-1: result-borne control signals, persisted so resume replays control flow faithfully —
196
+ * a classifier re-prunes its rejected branches, a recorded loop stop is honored. */
197
+ classify_branch?: string;
198
+ tournament_winner?: string;
199
+ loop_continue?: boolean;
200
+ /** W-1: the node's final output text — resume re-seeds the driver's outputs map from it so
201
+ * post-resume reduce/judge/dependent nodes still see their dependencies' outputs. */
202
+ output?: string;
242
203
  } | {
243
204
  kind: "workflow_nodes_submitted";
244
205
  turn: number;
245
- category?: KernelEventCategory;
246
- primitive?: KernelPrimitive;
247
206
  /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
248
207
  nodes: Record<string, unknown>[];
208
+ /** R3-1: graph base index the batch was appended at (from the kernel's
209
+ * WorkflowNodesSubmitted observation) — lets resume rebuild exact indices. */
210
+ base_index?: number;
211
+ /** W-N3: the submitting node's agent id (absent = host/bootstrap). Resume DROPS batches whose
212
+ * submitter re-runs — it will re-submit — instead of duplicating their nodes. */
213
+ submitter_agent_id?: string;
249
214
  } | {
250
215
  kind: "workflow_batch_spawned";
251
216
  turn: number;
252
- category?: KernelEventCategory;
253
- primitive?: KernelPrimitive;
254
217
  node_count: number;
255
218
  node_ids: string[];
256
219
  } | {
257
220
  kind: "workflow_completed";
258
221
  turn: number;
259
- category?: KernelEventCategory;
260
- primitive?: KernelPrimitive;
261
222
  completed: string[];
262
223
  failed: string[];
263
224
  total_nodes: number;
@@ -36,33 +36,56 @@ export declare function buildRunTerminalEvent(input: {
36
36
  }): Extract<SessionEvent, {
37
37
  kind: "run_terminal";
38
38
  }>;
39
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
40
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
39
41
  export declare function buildWorkflowNodeCompletedEvent(input: {
40
42
  turn: number;
41
43
  agentId: string;
42
44
  termination: string;
45
+ classifyBranch?: string;
46
+ tournamentWinner?: string;
47
+ loopContinue?: boolean;
48
+ output?: string;
43
49
  }): Extract<SessionEvent, {
44
50
  kind: "workflow_node_completed";
45
51
  }>;
52
+ /** One recovered node completion: the agent id plus its persisted control signals and output. */
53
+ export interface RecoveredNodeCompletion {
54
+ agentId: string;
55
+ classifyBranch?: string;
56
+ tournamentWinner?: string;
57
+ loopContinue?: boolean;
58
+ output?: string;
59
+ }
46
60
  /**
47
- * Recover completed workflow node agent_ids from a session event stream.
48
- * Scans for workflow_node_completed events and returns the agent_ids whose
49
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
61
+ * Recover completed workflow node records from a session event stream. Scans for
62
+ * workflow_node_completed events with termination "completed" and returns them WITH their
63
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
64
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
65
+ * driver's outputs map from the persisted output text.
50
66
  */
51
67
  export declare function recoverCompletedWorkflowNodes(events: Array<{
52
68
  seq: number;
53
69
  event: SessionEvent;
54
- }>): string[];
70
+ }>): RecoveredNodeCompletion[];
55
71
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
56
72
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
57
73
  export declare function buildWorkflowNodesSubmittedEvent(input: {
58
74
  turn: number;
59
75
  nodes: Record<string, unknown>[];
76
+ baseIndex?: number;
77
+ submitterAgentId?: string;
60
78
  }): Extract<SessionEvent, {
61
79
  kind: "workflow_nodes_submitted";
62
80
  }>;
63
81
  /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
64
- * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
82
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed.
83
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
65
84
  export declare function recoverSubmittedWorkflowNodes(events: Array<{
66
85
  seq: number;
67
86
  event: SessionEvent;
68
- }>): Record<string, unknown>[][];
87
+ }>): {
88
+ submissions: Record<string, unknown>[][];
89
+ bases: number[];
90
+ submitters: Array<string | undefined>;
91
+ };
@@ -48,24 +48,38 @@ export function buildRunTerminalEvent(input) {
48
48
  total_tokens: Math.max(0, input.totalTokens),
49
49
  };
50
50
  }
51
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
52
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
51
53
  export function buildWorkflowNodeCompletedEvent(input) {
52
54
  return {
53
55
  kind: "workflow_node_completed",
54
56
  turn: input.turn,
55
57
  agent_id: input.agentId,
56
58
  termination: input.termination,
59
+ ...(input.classifyBranch !== undefined ? { classify_branch: input.classifyBranch } : {}),
60
+ ...(input.tournamentWinner !== undefined ? { tournament_winner: input.tournamentWinner } : {}),
61
+ ...(input.loopContinue !== undefined ? { loop_continue: input.loopContinue } : {}),
62
+ ...(input.output ? { output: input.output } : {}),
57
63
  };
58
64
  }
59
65
  /**
60
- * Recover completed workflow node agent_ids from a session event stream.
61
- * Scans for workflow_node_completed events and returns the agent_ids whose
62
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
66
+ * Recover completed workflow node records from a session event stream. Scans for
67
+ * workflow_node_completed events with termination "completed" and returns them WITH their
68
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
69
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
70
+ * driver's outputs map from the persisted output text.
63
71
  */
64
72
  export function recoverCompletedWorkflowNodes(events) {
65
73
  const completed = [];
66
74
  for (const { event } of events) {
67
75
  if (event.kind === "workflow_node_completed" && event.termination === "completed") {
68
- completed.push(event.agent_id);
76
+ completed.push({
77
+ agentId: event.agent_id,
78
+ ...(event.classify_branch !== undefined ? { classifyBranch: event.classify_branch } : {}),
79
+ ...(event.tournament_winner !== undefined ? { tournamentWinner: event.tournament_winner } : {}),
80
+ ...(event.loop_continue !== undefined ? { loopContinue: event.loop_continue } : {}),
81
+ ...(event.output !== undefined ? { output: event.output } : {}),
82
+ });
69
83
  }
70
84
  }
71
85
  return completed;
@@ -73,15 +87,30 @@ export function recoverCompletedWorkflowNodes(events) {
73
87
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
74
88
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
75
89
  export function buildWorkflowNodesSubmittedEvent(input) {
76
- return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
90
+ return {
91
+ kind: "workflow_nodes_submitted",
92
+ turn: input.turn,
93
+ nodes: input.nodes,
94
+ ...(input.baseIndex !== undefined ? { base_index: input.baseIndex } : {}),
95
+ ...(input.submitterAgentId !== undefined ? { submitter_agent_id: input.submitterAgentId } : {}),
96
+ };
77
97
  }
78
98
  /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
79
- * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
99
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed.
100
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
80
101
  export function recoverSubmittedWorkflowNodes(events) {
81
102
  const submissions = [];
103
+ const bases = [];
104
+ const submitters = [];
82
105
  for (const { event } of events) {
83
- if (event.kind === "workflow_nodes_submitted")
106
+ if (event.kind === "workflow_nodes_submitted") {
84
107
  submissions.push(event.nodes);
108
+ submitters.push(event.submitter_agent_id);
109
+ // Absent on legacy logs → order-only replay (bases array stays parallel-short only
110
+ // if ALL records carry it; a mixed log degrades to order-only for safety).
111
+ if (event.base_index !== undefined)
112
+ bases.push(event.base_index);
113
+ }
85
114
  }
86
- return submissions;
115
+ return { submissions, bases: bases.length === submissions.length ? bases : [], submitters };
87
116
  }
@@ -10,6 +10,13 @@ export interface SubAgentRunContext {
10
10
  /** M5 v2.1: set when this child is a workflow node — propagated so a nested `start_workflow`
11
11
  * FLATTENS to the parent kernel rather than auto-pivoting into its own bootstrap. */
12
12
  isWorkflowNode?: boolean;
13
+ /** W-N1 tool exposure. The kernel omits an EMPTY `permitted_capability_ids` on the wire, so a
14
+ * grant-less workflow node is indistinguishable from a zero-cap spawn at the manifest — the
15
+ * caller states intent instead: `"inherit"` = run on the parent's execution plane with its
16
+ * meta-tool availability (trusted workflow nodes — they carried no grant list by design, and
17
+ * filtering on the missing list ran every DAG node TOOL-LESS); `"filtered"` (default) = filter
18
+ * to the manifest grants, empty ⇒ deny-all (spawn path, quarantined nodes). */
19
+ toolAccess?: "inherit" | "filtered";
13
20
  /** #2-B-ii: parent-controlled abort — when the kernel preempts this node (`AgentPreempted`), the
14
21
  * orchestrator interrupts the child runner, cancelling its in-flight LLM call. */
15
22
  abortSignal?: AbortSignal;
@@ -19,7 +19,9 @@ function terminationFromStatus(status) {
19
19
  normalized === "timeout" ||
20
20
  normalized === "user_abort" ||
21
21
  normalized === "error" ||
22
- normalized === "milestone_exceeded") {
22
+ normalized === "milestone_exceeded" ||
23
+ normalized === "context_overflow" ||
24
+ normalized === "no_progress") {
23
25
  return normalized;
24
26
  }
25
27
  return status;
@@ -47,12 +49,32 @@ function deriveMetaTools(permitted, opts) {
47
49
  metaTools.add("update_plan");
48
50
  return metaTools;
49
51
  }
52
+ /** W-N1: meta-tools by source availability alone — a `toolAccess: "inherit"` child gets the same
53
+ * meta surface a top-level run of these options would. */
54
+ function availableMetaTools(opts) {
55
+ const metaTools = new Set();
56
+ if (opts.skillContentMap?.size)
57
+ metaTools.add("skill");
58
+ if (opts.dreamStore)
59
+ metaTools.add("memory");
60
+ if (opts.knowledgeSource)
61
+ metaTools.add("knowledge");
62
+ if (opts.enablePlanTool)
63
+ metaTools.add("update_plan");
64
+ return metaTools;
65
+ }
50
66
  /** Host-side driver for kernel-isolated sub-agent runs. */
51
67
  export class SubAgentOrchestrator {
52
68
  async run(ctx) {
69
+ // W-N1: "inherit" runs the child on the parent's plane with availability-derived meta-tools
70
+ // (trusted workflow nodes); "filtered" (default) filters to the manifest grants, empty ⇒
71
+ // deny-all (spawn path, quarantined nodes).
72
+ const inherit = ctx.toolAccess === "inherit";
53
73
  const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
54
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
55
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
74
+ const metaTools = inherit ? availableMetaTools(ctx.parentOpts) : deriveMetaTools(permitted, ctx.parentOpts);
75
+ const execPlane = inherit
76
+ ? ctx.parentOpts.executionPlane
77
+ : new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
56
78
  let systemPrompt = ctx.parentOpts.systemPrompt;
57
79
  let inheritEvents;
58
80
  if (ctx.manifest.context_inheritance === "full") {
@@ -72,7 +94,10 @@ export class SubAgentOrchestrator {
72
94
  provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
73
95
  // M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
74
96
  maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
75
- executionPlane: filteredPlane,
97
+ // O3: per-child turn / wall-clock caps (fall back to the inherited limits).
98
+ maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
99
+ timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
100
+ executionPlane: execPlane,
76
101
  agentId: ctx.spec.identity.agentId,
77
102
  systemPrompt,
78
103
  sessionLog: ctx.sessionLog,
@@ -82,6 +107,13 @@ export class SubAgentOrchestrator {
82
107
  enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
83
108
  // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
84
109
  isWorkflowNode: ctx.isWorkflowNode,
110
+ // The child runs under ITS OWN spec, never the parent's: the spread above would otherwise
111
+ // leak the parent's `runSpec` (identity, capability filter — and an armed `loopRound`,
112
+ // giving every child a phantom pace tool). A loop-node iteration carries its own minimal
113
+ // spec to arm the pacing trap (DW-3); everything else runs spec-less as before.
114
+ runSpec: ctx.spec.loopRound
115
+ ? { identity: ctx.spec.identity, role: ctx.spec.role, goal: ctx.spec.goal, loopRound: ctx.spec.loopRound }
116
+ : undefined,
85
117
  });
86
118
  // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
87
119
  linkAbort(ctx.abortSignal, childRunner);
@@ -107,6 +139,9 @@ export class SubAgentOrchestrator {
107
139
  ...(finalText
108
140
  ? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } }
109
141
  : {}),
142
+ // DW-3: surface the kernel-adjudicated pace decision (loop-node iterations consume it as the
143
+ // continuation vocabulary). SDK-internal; stripped by subAgentResultToKernel.
144
+ ...(done?.paceDecision ? { paceDecision: done.paceDecision } : {}),
110
145
  };
111
146
  return {
112
147
  agentId: ctx.spec.identity.agentId,
@@ -2,7 +2,12 @@ import type { Message, ToolSchema } from "../../types.js";
2
2
  export type KernelAgentRole = "explore" | "plan" | "implement" | "verify" | "custom";
3
3
  export type AgentIsolation = "shared" | "read_only" | "worktree" | "remote";
4
4
  export type ContextInheritance = "none" | "system_only" | "full";
5
- export type TerminationReason = "completed" | "max_turns" | "token_budget" | "timeout" | "user_abort" | "error" | "milestone_exceeded";
5
+ export type TerminationReason = "completed" | "max_turns" | "token_budget" | "timeout" | "user_abort" | "error" | "milestone_exceeded"
6
+ /** v0.2.35 recovery ladder: compaction exhausted and the prompt still exceeds the provider window. */
7
+ | "context_overflow"
8
+ /** Repeat-fuse escalation: the same tool call (name AND args) re-issued past `terminateAfter` —
9
+ * a stall, distinct from `max_turns` which productive runs can also hit. */
10
+ | "no_progress";
6
11
  export type MilestonePolicy = "require_verifier" | "terminate" | "auto_pass";
7
12
  export interface AgentIdentity {
8
13
  agentId: string;
@@ -14,6 +19,16 @@ export interface AgentCapabilityFilter {
14
19
  allowedKinds?: string[];
15
20
  allowedIds?: string[];
16
21
  }
22
+ export interface LoopRoundSpec {
23
+ /** Hard round cap across the loop's lifetime; continue/sleep at the cap is coerced to stop. */
24
+ maxRounds?: number;
25
+ /** Sleep clamp floor (ms). */
26
+ minSleepMs?: number;
27
+ /** Sleep clamp ceiling (ms). */
28
+ maxSleepMs?: number;
29
+ /** Fallback when a round ends without a pace call: "stop" (goal loops, default) | "sleep" (cron loops). */
30
+ defaultAction?: "stop" | "sleep";
31
+ }
17
32
  export interface AgentRunSpec {
18
33
  identity: AgentIdentity;
19
34
  role: KernelAgentRole;
@@ -23,11 +38,18 @@ export interface AgentRunSpec {
23
38
  capabilityFilter?: AgentCapabilityFilter;
24
39
  milestones?: MilestoneContract;
25
40
  metadata?: Record<string, unknown>;
41
+ /** ③ loop-agent rounds: presence makes this run ONE round of a paced loop (gates the
42
+ * kernel `pace` meta-tool and arms the pacing trap). */
43
+ loopRound?: LoopRoundSpec;
26
44
  /** M1/G3: per-agent model preference; the host resolves it via `RuntimeOptions.providerFor`.
27
45
  * Host-side routing only — not sent to the kernel. */
28
46
  modelHint?: string;
29
47
  /** M4/G5: cumulative token cap for this sub-agent's run (sets the child kernel's `maxTotalTokens`). */
30
48
  tokenBudget?: number;
49
+ /** O3: per-child turn cap (sets the child runner's `maxTurns`; falls back to the parent's). */
50
+ maxTurns?: number;
51
+ /** O3: per-child wall-clock cap in ms (sets the child runner's `timeoutMs`; falls back to the parent's). */
52
+ maxWallMs?: number;
31
53
  }
32
54
  /** Kernel process-table observation (Phase 3 canonical spawn signal). */
33
55
  export interface AgentProcessChangedObservation {
@@ -61,6 +83,11 @@ export interface LoopResult {
61
83
  classifyBranch?: string;
62
84
  /** A#2 tournament verdict: a judge reports the winning entrant's agent id here. Sent only when set. */
63
85
  tournamentWinner?: string;
86
+ /** ③ loop-agent pacing: the kernel-adjudicated after-round decision, surfaced by the orchestrator
87
+ * from the child's done event. For a loop-node iteration this is the PRIMARY continuation
88
+ * vocabulary (stop → loopContinue=false); the legacy text-sniffed signal is the fallback.
89
+ * SDK-internal — stripped by `subAgentResultToKernel`. */
90
+ paceDecision?: import("../kernel-step.js").PaceDecision;
64
91
  }
65
92
  export interface SubAgentResult {
66
93
  agentId: string;
@@ -132,6 +159,10 @@ export interface WorkflowNodeSpec {
132
159
  };
133
160
  /** M4/G5: cap this node's child run at `tokenBudget` cumulative tokens (the per-node "use N tokens"). */
134
161
  tokenBudget?: number;
162
+ /** O3: cap this node's child run at `maxTurns` provider turns (falls back to the parent's). */
163
+ maxTurns?: number;
164
+ /** O3: cap this node's child run at `maxWallMs` wall-clock milliseconds. */
165
+ maxWallMs?: number;
135
166
  /** Indices of nodes this node depends on. */
136
167
  dependsOn?: number[];
137
168
  }
@@ -147,11 +178,14 @@ export interface WorkflowSpawnInfo {
147
178
  isolation: string;
148
179
  context_inheritance: string;
149
180
  model_hint?: string;
181
+ /** W3 trust level: `"trusted"` | `"quarantined"`. */
182
+ trust?: string;
150
183
  /** G3: JSON Schema the node's output must conform to (carried verbatim from the spec). */
151
184
  output_schema?: Record<string, unknown>;
152
185
  /** G2: for a reduce node, the registered reducer name to run (no LLM). */
153
186
  reducer?: string;
154
- /** G2: the dependency agent ids whose outputs a reduce node consumes. */
187
+ /** The dependency agent ids for EVERY dependent node (W-N2: a DAG edge carries data). A reduce
188
+ * node's registered function consumes them; every other node gets its deps' outputs in context. */
155
189
  input_agent_ids?: string[];
156
190
  /** A#2: present only for a tournament *judge* spawn — the two entrant agent ids whose produced
157
191
  * outputs this judge compares. The runner looks them up and reports the winner as `tournamentWinner`. */
@@ -167,6 +201,10 @@ export interface WorkflowSpawnInfo {
167
201
  classify_labels?: string[];
168
202
  /** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
169
203
  token_budget?: number;
204
+ /** O3: per-node turn cap → the child run's `maxTurns`. */
205
+ max_turns?: number;
206
+ /** O3: per-node wall-clock cap (ms) → the child run's timeout. */
207
+ max_wall_ms?: number;
170
208
  }
171
209
  /** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
172
210
  * `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
@@ -216,3 +254,9 @@ export declare function generateAndFilter(generators: WorkflowTaskSpec[], filter
216
254
  * re-checks flags. Verifiers run read-only with no inherited author context (bias-resistant).
217
255
  */
218
256
  export declare function verifyRules(rules: WorkflowTaskSpec[], skeptic?: WorkflowTaskSpec): WorkflowSpec;
257
+ /**
258
+ * Generate → evaluate loop template (parity with node/python `genEval`): a worker Loop node
259
+ * (worktree isolation, full context) feeding a fresh-context verifier whose output is pinned
260
+ * to the kernel's verdict schema. Async because the wasm kernel module loads lazily.
261
+ */
262
+ export declare function genEval(worker: WorkflowTaskSpec, evaluate: WorkflowTaskSpec, maxIters?: number, extractSkillOnPass?: boolean): Promise<WorkflowSpec>;
@@ -1,3 +1,4 @@
1
+ import { getKernel } from "../kernel.js";
1
2
  /** Map kernel spawn observation → host manifest. */
2
3
  export function spawnObservationToManifest(obs, spec, parentSessionId) {
3
4
  const o = obs;
@@ -45,6 +46,14 @@ export function agentRunSpecToKernel(spec) {
45
46
  out.verification_contract_id = spec.verificationContractId;
46
47
  if (spec.milestones)
47
48
  out.milestones = milestoneContractToKernel(spec.milestones);
49
+ if (spec.loopRound) {
50
+ out.loop_round = {
51
+ ...(spec.loopRound.maxRounds !== undefined ? { max_rounds: spec.loopRound.maxRounds } : {}),
52
+ ...(spec.loopRound.minSleepMs !== undefined ? { min_sleep_ms: spec.loopRound.minSleepMs } : {}),
53
+ ...(spec.loopRound.maxSleepMs !== undefined ? { max_sleep_ms: spec.loopRound.maxSleepMs } : {}),
54
+ ...(spec.loopRound.defaultAction !== undefined ? { default_action: spec.loopRound.defaultAction } : {}),
55
+ };
56
+ }
48
57
  return out;
49
58
  }
50
59
  export function milestoneContractToKernel(contract) {
@@ -175,6 +184,9 @@ export function workflowNodeSpecToKernel(n) {
175
184
  ...(kind ? { kind } : {}),
176
185
  // M4/G5: per-node token cap (additive; omitted when unset).
177
186
  ...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
187
+ // O3: per-node turn / wall-clock caps (additive; omitted when unset).
188
+ ...(n.maxTurns != null ? { max_turns: n.maxTurns } : {}),
189
+ ...(n.maxWallMs != null ? { max_wall_ms: n.maxWallMs } : {}),
178
190
  ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
179
191
  };
180
192
  }
@@ -302,10 +314,15 @@ export const startWorkflowTool = {
302
314
  };
303
315
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
304
316
  export function workflowNodeToSpec(node, parentSessionId) {
317
+ // W-N6 transcript-as-carry: a loop node's iterations share ONE stable session id (the `-i{k}`
318
+ // suffix names the spawn, not the session), so iteration k replays the transcript of 0..k-1 —
319
+ // "do the next increment" actually sees the previous increments. The agent_id keeps the
320
+ // per-iteration suffix (kernel completion routing).
321
+ const sessionNodeId = node.loop_max_iters != null ? node.agent_id.replace(/-i\d+$/, "") : node.agent_id;
305
322
  return {
306
323
  identity: {
307
324
  agentId: node.agent_id,
308
- sessionId: `${parentSessionId}-${node.agent_id}`,
325
+ sessionId: `${parentSessionId}-${sessionNodeId}`,
309
326
  isSubAgent: true,
310
327
  parentSessionId,
311
328
  },
@@ -316,6 +333,14 @@ export function workflowNodeToSpec(node, parentSessionId) {
316
333
  ...(node.model_hint ? { modelHint: node.model_hint } : {}),
317
334
  // M4/G5: carry the node's token cap so the orchestrator can bound the child run.
318
335
  ...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
336
+ // O3: carry the node's turn / wall-clock caps (the orchestrator already honors these).
337
+ ...(node.max_turns != null ? { maxTurns: node.max_turns } : {}),
338
+ ...(node.max_wall_ms != null ? { maxWallMs: node.max_wall_ms } : {}),
339
+ // DW-3 one continuation vocabulary: a loop ITERATION runs with the pacing trap armed, so the
340
+ // agent signals continue/stop through the kernel-adjudicated `pace` meta-tool instead of a
341
+ // text-sniffed JSON blob. One iteration = one round; the DAG (not max_rounds) caps iterations,
342
+ // and default stop means "ended without pacing" = done — the CC silence-is-completion contract.
343
+ ...(node.loop_max_iters != null ? { loopRound: { defaultAction: "stop" } } : {}),
319
344
  };
320
345
  }
321
346
  /** Build the host manifest for a kernel-generated workflow node. */
@@ -391,3 +416,31 @@ export function verifyRules(rules, skeptic) {
391
416
  }
392
417
  return { nodes };
393
418
  }
419
+ /**
420
+ * Generate → evaluate loop template (parity with node/python `genEval`): a worker Loop node
421
+ * (worktree isolation, full context) feeding a fresh-context verifier whose output is pinned
422
+ * to the kernel's verdict schema. Async because the wasm kernel module loads lazily.
423
+ */
424
+ export async function genEval(worker, evaluate, maxIters = 3, extractSkillOnPass = true) {
425
+ const kernel = await getKernel();
426
+ const schema = JSON.parse(kernel.verdictOutputSchema(extractSkillOnPass));
427
+ return {
428
+ nodes: [
429
+ {
430
+ task: asTask(worker),
431
+ role: "implement",
432
+ isolation: "worktree",
433
+ contextInheritance: "full",
434
+ loop: { maxIters: Math.max(1, maxIters) },
435
+ },
436
+ {
437
+ task: asTask(evaluate),
438
+ role: "verify",
439
+ isolation: "read_only",
440
+ contextInheritance: "none",
441
+ dependsOn: [0],
442
+ outputSchema: schema,
443
+ },
444
+ ],
445
+ };
446
+ }
@@ -1,5 +1,13 @@
1
- /** Instruction appended to a loop node's goal: do the next increment, and signal when done. */
2
- export declare function loopInstruction(maxIters: number): string;
1
+ /** Instruction appended to a loop iteration's goal. DW-3: the continuation verb is the
2
+ * kernel-adjudicated `pace` meta-tool (armed on every iteration run), not a text blob — one
3
+ * vocabulary shared with the round-level loop agent. Iterations share one session, so "the work
4
+ * so far" is simply the visible transcript. */
5
+ export declare function loopInstruction(maxIters: number, iteration?: number): string;
6
+ /** W-N2: dependency outputs appended to a dependent node's goal — a DAG edge carries data, not
7
+ * just ordering (fan-out→synthesize was an uninformed synthesis without this). Each dependency's
8
+ * output is clipped so a chain of large nodes can't blow the child's context; empty/unknown
9
+ * outputs are skipped. Returns "" when the node has no dependencies. */
10
+ export declare function dependencyOutputsNote(inputAgentIds: string[] | undefined, outputs: Map<string, string> | undefined, maxPerDep?: number): string;
3
11
  /** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
4
12
  export declare function classifyInstruction(labels: string[]): string;
5
13
  /** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */