@deepstrike/sdk 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.
Files changed (35) hide show
  1. package/README.md +6 -6
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/os/public.d.ts +1 -1
  5. package/dist/os/public.js +1 -1
  6. package/dist/runtime/facade.js +11 -11
  7. package/dist/runtime/kernel-event-log.d.ts +0 -6
  8. package/dist/runtime/kernel-event-log.js +38 -62
  9. package/dist/runtime/kernel-step.d.ts +11 -0
  10. package/dist/runtime/kernel-step.js +12 -0
  11. package/dist/runtime/large-result-spool.d.ts +7 -0
  12. package/dist/runtime/large-result-spool.js +25 -0
  13. package/dist/runtime/loop-driver.d.ts +108 -0
  14. package/dist/runtime/loop-driver.js +198 -0
  15. package/dist/runtime/os-snapshot.d.ts +0 -1
  16. package/dist/runtime/os-snapshot.js +0 -19
  17. package/dist/runtime/reactive-session.d.ts +5 -2
  18. package/dist/runtime/reactive-session.js +17 -4
  19. package/dist/runtime/run-group.d.ts +9 -0
  20. package/dist/runtime/run-group.js +18 -4
  21. package/dist/runtime/runner.d.ts +151 -12
  22. package/dist/runtime/runner.js +568 -155
  23. package/dist/runtime/session-log.d.ts +31 -54
  24. package/dist/runtime/session-repair.d.ts +29 -7
  25. package/dist/runtime/session-repair.js +37 -9
  26. package/dist/runtime/sub-agent-orchestrator.d.ts +12 -0
  27. package/dist/runtime/sub-agent-orchestrator.js +54 -30
  28. package/dist/runtime/workflow-control-flow.d.ts +10 -2
  29. package/dist/runtime/workflow-control-flow.js +27 -6
  30. package/dist/signals/gateway.d.ts +4 -2
  31. package/dist/signals/gateway.js +8 -1
  32. package/dist/types/agent.d.ts +40 -2
  33. package/dist/types/agent.js +25 -1
  34. package/dist/types.d.ts +2 -0
  35. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
+ import type { KernelPrimitive } from "./kernel-event-log.js";
1
2
  import type { ContentPart, ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
2
- import type { KernelEventCategory, KernelPrimitive } from "./kernel-event-log.js";
3
3
  export type RollbackReason = {
4
4
  kind: "fatal_tool_error";
5
5
  tool_name: string;
@@ -75,8 +75,6 @@ export type SessionEvent = {
75
75
  } | {
76
76
  kind: "compressed";
77
77
  turn: number;
78
- category?: KernelEventCategory;
79
- primitive?: KernelPrimitive;
80
78
  archived_seq_range: [number, number];
81
79
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
82
80
  summary?: string;
@@ -86,8 +84,6 @@ export type SessionEvent = {
86
84
  } | {
87
85
  kind: "page_out";
88
86
  turn: number;
89
- category?: KernelEventCategory;
90
- primitive?: KernelPrimitive;
91
87
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
92
88
  summary?: string;
93
89
  tier_hint?: string;
@@ -95,14 +91,10 @@ export type SessionEvent = {
95
91
  } | {
96
92
  kind: "page_in";
97
93
  turn: number;
98
- category?: KernelEventCategory;
99
- primitive?: KernelPrimitive;
100
94
  entry_count: number;
101
95
  } | {
102
96
  kind: "large_result_spooled";
103
97
  turn: number;
104
- category?: KernelEventCategory;
105
- primitive?: KernelPrimitive;
106
98
  call_id: string;
107
99
  tool: string;
108
100
  original_size: number;
@@ -111,15 +103,11 @@ export type SessionEvent = {
111
103
  } | {
112
104
  kind: "rollbacked";
113
105
  turn: number;
114
- category?: KernelEventCategory;
115
- primitive?: KernelPrimitive;
116
106
  checkpoint_history_len: number;
117
107
  reason?: RollbackReason;
118
108
  } | {
119
109
  kind: "capability_changed";
120
110
  turn: number;
121
- category?: KernelEventCategory;
122
- primitive?: KernelPrimitive;
123
111
  added: string[];
124
112
  removed: string[];
125
113
  change_kind?: string;
@@ -130,78 +118,51 @@ export type SessionEvent = {
130
118
  } | {
131
119
  kind: "context_renewed";
132
120
  turn: number;
133
- category?: KernelEventCategory;
134
- primitive?: KernelPrimitive;
135
121
  sprint: number;
136
122
  handoff_ref: string;
137
123
  } | {
138
124
  kind: "suspended";
139
125
  turn: number;
140
- category?: KernelEventCategory;
141
- primitive?: KernelPrimitive;
142
126
  reason: string;
143
127
  pending_calls?: string[];
144
128
  } | {
145
129
  kind: "resumed";
146
130
  turn: number;
147
- category?: KernelEventCategory;
148
- primitive?: KernelPrimitive;
149
131
  approved?: string[];
150
132
  denied?: string[];
151
133
  } | {
152
134
  kind: "tool_gated";
153
135
  turn: number;
154
- category?: KernelEventCategory;
155
- primitive?: KernelPrimitive;
156
136
  call_id: string;
157
137
  tool: string;
158
138
  reason: string;
159
139
  } | {
160
140
  kind: "signal_disposed";
161
141
  turn: number;
162
- category?: KernelEventCategory;
163
- primitive?: KernelPrimitive;
164
142
  signal_id: string;
165
143
  disposition: string;
166
144
  queue_depth: number;
167
145
  } | {
168
146
  kind: "budget_exceeded";
169
147
  turn: number;
170
- category?: KernelEventCategory;
171
- primitive?: KernelPrimitive;
172
148
  budget: string;
173
149
  } | {
174
150
  kind: "milestone_advanced";
175
151
  turn: number;
176
- category?: KernelEventCategory;
177
- primitive?: KernelPrimitive;
178
152
  phase_id: string;
179
153
  capabilities_unlocked: string[];
180
154
  } | {
181
155
  kind: "milestone_blocked";
182
156
  turn: number;
183
- category?: KernelEventCategory;
184
- primitive?: KernelPrimitive;
185
157
  phase_id: string;
186
158
  reason: string;
187
- } | {
188
- kind: "milestone_evidence";
189
- turn: number;
190
- category?: KernelEventCategory;
191
- primitive?: KernelPrimitive;
192
- phase_id: string;
193
- evidence: string[];
194
159
  } | {
195
160
  kind: "checkpoint_taken";
196
161
  turn: number;
197
- category?: KernelEventCategory;
198
- primitive?: KernelPrimitive;
199
162
  history_len: number;
200
163
  } | {
201
164
  kind: "agent_process_changed";
202
165
  turn: number;
203
- category?: KernelEventCategory;
204
- primitive?: KernelPrimitive;
205
166
  agent_id: string;
206
167
  parent_session_id: string;
207
168
  role: string;
@@ -213,24 +174,18 @@ export type SessionEvent = {
213
174
  } | {
214
175
  kind: "memory_written";
215
176
  turn: number;
216
- category?: KernelEventCategory;
217
- primitive?: KernelPrimitive;
218
177
  memory_id: string;
219
178
  memory_kind: string;
220
179
  size_bytes: number;
221
180
  } | {
222
181
  kind: "memory_queried";
223
182
  turn: number;
224
- category?: KernelEventCategory;
225
- primitive?: KernelPrimitive;
226
183
  query_context: string;
227
184
  requested_k: number;
228
185
  requires_async_response: boolean;
229
186
  } | {
230
187
  kind: "memory_validation_failed";
231
188
  turn: number;
232
- category?: KernelEventCategory;
233
- primitive?: KernelPrimitive;
234
189
  memory_id: string;
235
190
  error: string;
236
191
  } | {
@@ -240,29 +195,35 @@ export type SessionEvent = {
240
195
  } | {
241
196
  kind: "workflow_node_completed";
242
197
  turn: number;
243
- category?: KernelEventCategory;
244
- primitive?: KernelPrimitive;
245
198
  agent_id: string;
246
199
  termination: string;
200
+ /** W-1: result-borne control signals, persisted so resume replays control flow faithfully —
201
+ * a classifier re-prunes its rejected branches, a recorded loop stop is honored. */
202
+ classify_branch?: string;
203
+ tournament_winner?: string;
204
+ loop_continue?: boolean;
205
+ /** W-1: the node's final output text — resume re-seeds the driver's outputs map from it so
206
+ * post-resume reduce/judge/dependent nodes still see their dependencies' outputs. */
207
+ output?: string;
247
208
  } | {
248
209
  kind: "workflow_nodes_submitted";
249
210
  turn: number;
250
- category?: KernelEventCategory;
251
- primitive?: KernelPrimitive;
252
211
  /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
253
212
  nodes: Record<string, unknown>[];
213
+ /** R3-1: graph base index the batch was appended at (from the kernel's
214
+ * WorkflowNodesSubmitted observation) — lets resume rebuild exact indices. */
215
+ base_index?: number;
216
+ /** W-N3: the submitting node's agent id (absent = host/bootstrap). Resume DROPS batches whose
217
+ * submitter re-runs — it will re-submit — instead of duplicating their nodes. */
218
+ submitter_agent_id?: string;
254
219
  } | {
255
220
  kind: "workflow_batch_spawned";
256
221
  turn: number;
257
- category?: KernelEventCategory;
258
- primitive?: KernelPrimitive;
259
222
  node_count: number;
260
223
  node_ids: string[];
261
224
  } | {
262
225
  kind: "workflow_completed";
263
226
  turn: number;
264
- category?: KernelEventCategory;
265
- primitive?: KernelPrimitive;
266
227
  completed: string[];
267
228
  failed: string[];
268
229
  total_nodes: number;
@@ -279,10 +240,26 @@ export type SessionEvent = {
279
240
  kind: "group_member_joined";
280
241
  session_id: string;
281
242
  role?: string;
243
+ member_kind?: "peer" | "vehicle";
282
244
  } | {
283
245
  kind: "group_budget_charged";
284
246
  tokens: number;
285
247
  subagents: number;
248
+ rounds?: number;
249
+ } | {
250
+ kind: "round_started";
251
+ /** 1-based round number within the loop. */
252
+ round: number;
253
+ goal: string;
254
+ } | {
255
+ kind: "round_paced";
256
+ round: number;
257
+ action: "continue" | "sleep" | "stop";
258
+ delay_ms?: number;
259
+ /** Absolute wake time for sleep — lets a stateless host re-arm from the log alone. */
260
+ wake_at_ms?: number;
261
+ reason: string;
262
+ coerced_from?: string;
286
263
  };
287
264
  export interface SessionLog {
288
265
  append(sessionId: string, event: SessionEvent): Promise<number>;
@@ -41,34 +41,56 @@ export declare function buildRunTerminalEvent(input: {
41
41
  }): Extract<SessionEvent, {
42
42
  kind: "run_terminal";
43
43
  }>;
44
- /** Build workflow_node_completed for persistence after a node finishes. */
44
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
45
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
45
46
  export declare function buildWorkflowNodeCompletedEvent(input: {
46
47
  turn: number;
47
48
  agentId: string;
48
49
  termination: string;
50
+ classifyBranch?: string;
51
+ tournamentWinner?: string;
52
+ loopContinue?: boolean;
53
+ output?: string;
49
54
  }): Extract<SessionEvent, {
50
55
  kind: "workflow_node_completed";
51
56
  }>;
57
+ /** One recovered node completion: the agent id plus its persisted control signals and output. */
58
+ export interface RecoveredNodeCompletion {
59
+ agentId: string;
60
+ classifyBranch?: string;
61
+ tournamentWinner?: string;
62
+ loopContinue?: boolean;
63
+ output?: string;
64
+ }
52
65
  /**
53
- * Recover completed workflow node agent_ids from a session event stream.
54
- * Scans for workflow_node_completed events and returns the agent_ids whose
55
- * 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.
56
71
  */
57
72
  export declare function recoverCompletedWorkflowNodes(events: Array<{
58
73
  seq: number;
59
74
  event: SessionEvent;
60
- }>): string[];
75
+ }>): RecoveredNodeCompletion[];
61
76
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
62
77
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
63
78
  export declare function buildWorkflowNodesSubmittedEvent(input: {
64
79
  turn: number;
65
80
  nodes: Record<string, unknown>[];
81
+ baseIndex?: number;
82
+ submitterAgentId?: string;
66
83
  }): Extract<SessionEvent, {
67
84
  kind: "workflow_nodes_submitted";
68
85
  }>;
69
86
  /** R3-1: recover the runtime submission batches (in order) from a session event stream, to rebuild
70
- * `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed. */
87
+ * `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed.
88
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
71
89
  export declare function recoverSubmittedWorkflowNodes(events: Array<{
72
90
  seq: number;
73
91
  event: SessionEvent;
74
- }>): Record<string, unknown>[][];
92
+ }>): {
93
+ submissions: Record<string, unknown>[][];
94
+ bases: number[];
95
+ submitters: Array<string | undefined>;
96
+ };
@@ -53,25 +53,38 @@ export function buildRunTerminalEvent(input) {
53
53
  total_tokens: Math.max(0, input.totalTokens),
54
54
  };
55
55
  }
56
- /** Build workflow_node_completed for persistence after a node finishes. */
56
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
57
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
57
58
  export function buildWorkflowNodeCompletedEvent(input) {
58
59
  return {
59
60
  kind: "workflow_node_completed",
60
61
  turn: input.turn,
61
62
  agent_id: input.agentId,
62
63
  termination: input.termination,
64
+ ...(input.classifyBranch !== undefined ? { classify_branch: input.classifyBranch } : {}),
65
+ ...(input.tournamentWinner !== undefined ? { tournament_winner: input.tournamentWinner } : {}),
66
+ ...(input.loopContinue !== undefined ? { loop_continue: input.loopContinue } : {}),
67
+ ...(input.output ? { output: input.output } : {}),
63
68
  };
64
69
  }
65
70
  /**
66
- * Recover completed workflow node agent_ids from a session event stream.
67
- * Scans for workflow_node_completed events and returns the agent_ids whose
68
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
71
+ * Recover completed workflow node records from a session event stream. Scans for
72
+ * workflow_node_completed events with termination "completed" and returns them WITH their
73
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
74
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
75
+ * driver's outputs map from the persisted output text.
69
76
  */
70
77
  export function recoverCompletedWorkflowNodes(events) {
71
78
  const completed = [];
72
79
  for (const { event } of events) {
73
80
  if (event.kind === "workflow_node_completed" && event.termination === "completed") {
74
- completed.push(event.agent_id);
81
+ completed.push({
82
+ agentId: event.agent_id,
83
+ ...(event.classify_branch !== undefined ? { classifyBranch: event.classify_branch } : {}),
84
+ ...(event.tournament_winner !== undefined ? { tournamentWinner: event.tournament_winner } : {}),
85
+ ...(event.loop_continue !== undefined ? { loopContinue: event.loop_continue } : {}),
86
+ ...(event.output !== undefined ? { output: event.output } : {}),
87
+ });
75
88
  }
76
89
  }
77
90
  return completed;
@@ -79,15 +92,30 @@ export function recoverCompletedWorkflowNodes(events) {
79
92
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
80
93
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
81
94
  export function buildWorkflowNodesSubmittedEvent(input) {
82
- return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
95
+ return {
96
+ kind: "workflow_nodes_submitted",
97
+ turn: input.turn,
98
+ nodes: input.nodes,
99
+ ...(input.baseIndex !== undefined ? { base_index: input.baseIndex } : {}),
100
+ ...(input.submitterAgentId !== undefined ? { submitter_agent_id: input.submitterAgentId } : {}),
101
+ };
83
102
  }
84
103
  /** R3-1: recover the runtime submission batches (in order) from a session event stream, to rebuild
85
- * `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed. */
104
+ * `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed.
105
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
86
106
  export function recoverSubmittedWorkflowNodes(events) {
87
107
  const submissions = [];
108
+ const bases = [];
109
+ const submitters = [];
88
110
  for (const { event } of events) {
89
- if (event.kind === "workflow_nodes_submitted")
111
+ if (event.kind === "workflow_nodes_submitted") {
90
112
  submissions.push(event.nodes);
113
+ submitters.push(event.submitter_agent_id);
114
+ // Absent on legacy logs → order-only replay (bases array stays parallel-short only
115
+ // if ALL records carry it; a mixed log degrades to order-only for safety).
116
+ if (event.base_index !== undefined)
117
+ bases.push(event.base_index);
118
+ }
91
119
  }
92
- return submissions;
120
+ return { submissions, bases: bases.length === submissions.length ? bases : [], submitters };
93
121
  }
@@ -16,6 +16,13 @@ export interface SubAgentRunContext {
16
16
  * the child runner so a nested `start_workflow` FLATTENS to the parent kernel rather than
17
17
  * auto-pivoting into its own bootstrap (which would fragment the one-kernel/one-quota governance). */
18
18
  isWorkflowNode?: boolean;
19
+ /** W-N1 tool exposure. The kernel omits an EMPTY `permitted_capability_ids` on the wire, so a
20
+ * grant-less workflow node is indistinguishable from a zero-cap spawn at the manifest — the
21
+ * caller states intent instead: `"inherit"` = run on the parent's execution plane with its
22
+ * meta-tool availability (trusted workflow nodes — they carried no grant list by design, and
23
+ * filtering on the missing list ran every DAG node TOOL-LESS); `"filtered"` (default) = filter
24
+ * to the manifest grants, empty ⇒ deny-all (spawn path, quarantined nodes). */
25
+ toolAccess?: "inherit" | "filtered";
19
26
  /** #2-B-ii: parent-controlled abort. When this fires (the kernel preempted this node via
20
27
  * `InterruptNow` → `AgentPreempted`), the orchestrator interrupts the child runner, cancelling its
21
28
  * in-flight LLM call. */
@@ -26,6 +33,11 @@ export interface SubAgentRunContext {
26
33
  export declare function resolveProvider(opts: RuntimeOptions, modelHint?: string): RuntimeOptions["provider"];
27
34
  /** Host-side driver for kernel-isolated sub-agent runs. */
28
35
  export declare class SubAgentOrchestrator {
36
+ /** W-N4: the ONE child-runner construction shared by the direct-stream and harness paths — tool
37
+ * grant resolution (W-N1), worktree wrapping, inherited system prompt / events, per-node caps.
38
+ * The two used to be near-identical copies that had already drifted (the harness path silently
39
+ * dropped context inheritance). */
40
+ private buildChild;
29
41
  stream(ctx: SubAgentRunContext): AsyncIterable<StreamEvent>;
30
42
  run(ctx: SubAgentRunContext): Promise<SubAgentResult>;
31
43
  }
@@ -10,7 +10,9 @@ function terminationFromStatus(status) {
10
10
  normalized === "timeout" ||
11
11
  normalized === "user_abort" ||
12
12
  normalized === "error" ||
13
- normalized === "milestone_exceeded") {
13
+ normalized === "milestone_exceeded" ||
14
+ normalized === "context_overflow" ||
15
+ normalized === "no_progress") {
14
16
  return normalized;
15
17
  }
16
18
  return status;
@@ -60,14 +62,38 @@ function deriveMetaTools(permitted, opts) {
60
62
  metaTools.add("update_plan");
61
63
  return metaTools;
62
64
  }
65
+ /** W-N1: meta-tools by source availability alone — a `toolAccess: "inherit"` child gets the same
66
+ * meta surface a top-level run of these options would. */
67
+ function availableMetaTools(opts) {
68
+ const metaTools = new Set();
69
+ if (opts.skillDir)
70
+ metaTools.add("skill");
71
+ if (opts.dreamStore)
72
+ metaTools.add("memory");
73
+ if (opts.knowledgeSource)
74
+ metaTools.add("knowledge");
75
+ if (opts.enablePlanTool)
76
+ metaTools.add("update_plan");
77
+ return metaTools;
78
+ }
63
79
  /** Host-side driver for kernel-isolated sub-agent runs. */
64
80
  export class SubAgentOrchestrator {
65
- async *stream(ctx) {
81
+ /** W-N4: the ONE child-runner construction shared by the direct-stream and harness paths — tool
82
+ * grant resolution (W-N1), worktree wrapping, inherited system prompt / events, per-node caps.
83
+ * The two used to be near-identical copies that had already drifted (the harness path silently
84
+ * dropped context inheritance). */
85
+ async buildChild(ctx) {
86
+ // W-N1: "inherit" runs the child on the parent's plane with availability-derived meta-tools
87
+ // (trusted workflow nodes); "filtered" (default) filters to the manifest grants, empty ⇒
88
+ // deny-all (spawn path, quarantined nodes).
89
+ const inherit = ctx.toolAccess === "inherit";
66
90
  const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
67
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
68
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
69
- // M3/G4: a worktree node runs inside its own git worktree (created here, removed in `finally`).
70
- const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, filteredPlane);
91
+ const metaTools = inherit ? availableMetaTools(ctx.parentOpts) : deriveMetaTools(permitted, ctx.parentOpts);
92
+ const basePlane = inherit
93
+ ? ctx.parentOpts.executionPlane
94
+ : new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
95
+ // M3/G4: a worktree node runs inside its own git worktree (created here, removed by the caller).
96
+ const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, basePlane);
71
97
  let systemPrompt = ctx.parentOpts.systemPrompt;
72
98
  let inheritEvents;
73
99
  if (ctx.manifest.context_inheritance === "full") {
@@ -87,6 +113,9 @@ export class SubAgentOrchestrator {
87
113
  provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
88
114
  // M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
89
115
  maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
116
+ // O3: per-child turn / wall-clock caps (fall back to the inherited limits).
117
+ maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
118
+ timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
90
119
  executionPlane: execPlane,
91
120
  agentId: ctx.spec.identity.agentId,
92
121
  systemPrompt,
@@ -97,10 +126,21 @@ export class SubAgentOrchestrator {
97
126
  enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
98
127
  // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
99
128
  isWorkflowNode: ctx.isWorkflowNode,
129
+ // The child runs under ITS OWN spec, never the parent's: the spread above would otherwise
130
+ // leak the parent's `runSpec` (identity, capability filter — and a LoopDriver's armed
131
+ // `loopRound`, giving every child a phantom pace tool). A loop-node iteration carries its
132
+ // own minimal spec to arm the pacing trap (DW-3); everything else runs spec-less as before.
133
+ runSpec: ctx.spec.loopRound
134
+ ? { identity: ctx.spec.identity, role: ctx.spec.role, goal: ctx.spec.goal, loopRound: ctx.spec.loopRound }
135
+ : undefined,
100
136
  });
101
137
  // #2-B-ii: when the parent preempts this node (kernel `AgentPreempted`), interrupt the child —
102
138
  // cancelling its in-flight LLM call. Handle an already-aborted signal too (creation race).
103
139
  linkAbort(ctx.abortSignal, childRunner);
140
+ return { childRunner, inheritEvents, cleanupWorktree };
141
+ }
142
+ async *stream(ctx) {
143
+ const { childRunner, inheritEvents, cleanupWorktree } = await this.buildChild(ctx);
104
144
  try {
105
145
  yield* childRunner.run({
106
146
  sessionId: ctx.spec.identity.sessionId,
@@ -114,31 +154,12 @@ export class SubAgentOrchestrator {
114
154
  }
115
155
  async run(ctx) {
116
156
  if (ctx.harness) {
117
- const { RuntimeRunner } = await import("./runner.js");
118
157
  const { HarnessLoop } = await import("../harness/harness.js");
119
- const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
120
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
121
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
122
- // M3/G4: worktree isolation for a worktree node (cleaned up in `finally` below).
123
- const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, filteredPlane);
124
- const childRunner = new RuntimeRunner({
125
- ...ctx.parentOpts,
126
- // M1/G3: route to the node's hinted model (falls back to the parent provider).
127
- provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
128
- // M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
129
- maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
130
- executionPlane: execPlane,
131
- agentId: ctx.spec.identity.agentId,
132
- sessionLog: ctx.sessionLog,
133
- skillDir: metaTools.has("skill") ? ctx.parentOpts.skillDir : undefined,
134
- dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
135
- knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
136
- enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
137
- // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
138
- isWorkflowNode: ctx.isWorkflowNode,
139
- });
140
- // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
141
- linkAbort(ctx.abortSignal, childRunner);
158
+ // W-N4: same construction as the direct path — the harness child now honors the inherited
159
+ // system prompt (`system_only`). NOTE: `full` inheritance is NOT consumable under a harness
160
+ // (HarnessLoop mints its own per-attempt session ids, so parent events can't be replayed)
161
+ // the inherited events are dropped here by construction, not by drift.
162
+ const { childRunner, cleanupWorktree } = await this.buildChild(ctx);
142
163
  const loop = new HarnessLoop(childRunner, ctx.harness.evalProvider, {
143
164
  maxAttempts: ctx.harness.maxAttempts ?? 3,
144
165
  });
@@ -186,6 +207,9 @@ export class SubAgentOrchestrator {
186
207
  turnsUsed: done?.iterations ?? 0,
187
208
  totalTokensUsed: done?.totalTokens ?? 0,
188
209
  ...(finalText ? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } } : {}),
210
+ // DW-3: surface the kernel-adjudicated pace decision (loop-node iterations consume it as the
211
+ // continuation vocabulary). SDK-internal; stripped by subAgentResultToKernel.
212
+ ...(done?.paceDecision ? { paceDecision: done.paceDecision } : {}),
189
213
  };
190
214
  return {
191
215
  agentId: ctx.spec.identity.agentId,
@@ -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. */
@@ -6,12 +6,33 @@
6
6
  //! decision from the node's agent and extracts the matching result signal (`loopContinue` /
7
7
  //! `classifyBranch` / `tournamentWinner`) the kernel reads back.
8
8
  import { extractJsonValue } from "./output-schema.js";
9
- /** Instruction appended to a loop node's goal: do the next increment, and signal when done. */
10
- export function loopInstruction(maxIters) {
11
- return (`This task runs as a LOOP (up to ${maxIters} iterations total). Do the next increment of work now. ` +
12
- `When you judge the overall task COMPLETE and no further iterations are needed, end your response ` +
13
- `with a JSON object {"loop_continue": false}. To request another iteration, omit it or return ` +
14
- `{"loop_continue": true}.`);
9
+ /** Instruction appended to a loop iteration's goal. DW-3: the continuation verb is the
10
+ * kernel-adjudicated `pace` meta-tool (armed on every iteration run), not a text blob — one
11
+ * vocabulary shared with the round-level loop agent. Iterations share one session, so "the work
12
+ * so far" is simply the visible transcript. */
13
+ export function loopInstruction(maxIters, iteration = 0) {
14
+ return (`This task runs as a LOOP — this is iteration ${iteration + 1} of up to ${maxIters}. Your prior ` +
15
+ `iterations' work (if any) is visible above; do the NEXT increment now. Then call the \`pace\` tool: ` +
16
+ `\`{"next": "continue"}\` to request another iteration, or \`{"next": "stop"}\` when the overall ` +
17
+ `task is complete. Ending without calling \`pace\` also completes the loop.`);
18
+ }
19
+ /** W-N2: dependency outputs appended to a dependent node's goal — a DAG edge carries data, not
20
+ * just ordering (fan-out→synthesize was an uninformed synthesis without this). Each dependency's
21
+ * output is clipped so a chain of large nodes can't blow the child's context; empty/unknown
22
+ * outputs are skipped. Returns "" when the node has no dependencies. */
23
+ export function dependencyOutputsNote(inputAgentIds, outputs, maxPerDep = 8_000) {
24
+ if (!inputAgentIds?.length || !outputs)
25
+ return "";
26
+ const blocks = inputAgentIds
27
+ .map(id => {
28
+ const out = outputs.get(id) ?? "";
29
+ if (!out)
30
+ return "";
31
+ const clipped = out.length > maxPerDep ? `${out.slice(0, maxPerDep)}\n…[truncated]` : out;
32
+ return `[dependency ${id} output]\n${clipped}`;
33
+ })
34
+ .filter(Boolean);
35
+ return blocks.join("\n\n");
15
36
  }
16
37
  /** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
17
38
  export function classifyInstruction(labels) {
@@ -23,8 +23,10 @@ export declare class SignalGateway implements SignalSource {
23
23
  * shared gateway can serve N peer loops. Omit ⇒ legacy FIFO drain (any signal).
24
24
  */
25
25
  nextSignal(recipient?: string): Promise<RuntimeSignal | null>;
26
- /** Register a listener that is called synchronously whenever a signal is emitted. */
27
- onSignal(listener: (sig: RuntimeSignal) => void): void;
26
+ /** Register a listener that is called synchronously whenever a signal is emitted.
27
+ * Returns an unsubscribe function long-lived consumers (e.g. a loop's
28
+ * `signalAwareSleeper`, re-registered per sleep) must call it or the listener leaks. */
29
+ onSignal(listener: (sig: RuntimeSignal) => void): () => void;
28
30
  /** Schedule a ScheduledPrompt to fire at its `runAtMs`. Idempotent by goal+time. */
29
31
  schedule(prompt: ScheduledPrompt): void;
30
32
  /** Cancel a scheduled prompt before it fires. */
@@ -30,9 +30,16 @@ export class SignalGateway {
30
30
  return this.queue.splice(idx, 1)[0];
31
31
  }
32
32
  // ── Push API ────────────────────────────────────────────────────────────────
33
- /** Register a listener that is called synchronously whenever a signal is emitted. */
33
+ /** Register a listener that is called synchronously whenever a signal is emitted.
34
+ * Returns an unsubscribe function — long-lived consumers (e.g. a loop's
35
+ * `signalAwareSleeper`, re-registered per sleep) must call it or the listener leaks. */
34
36
  onSignal(listener) {
35
37
  this.listeners.push(listener);
38
+ return () => {
39
+ const idx = this.listeners.indexOf(listener);
40
+ if (idx !== -1)
41
+ this.listeners.splice(idx, 1);
42
+ };
36
43
  }
37
44
  /** Schedule a ScheduledPrompt to fire at its `runAtMs`. Idempotent by goal+time. */
38
45
  schedule(prompt) {