@deepstrike/sdk 0.2.36 → 0.2.38

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
+ 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,66 @@ 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;
163
+ } | {
164
+ kind: "entropy_sample";
165
+ turn: number;
166
+ score: number;
167
+ score_version: number;
168
+ rho: number;
169
+ repeat_pressure: number;
170
+ failure_rate: number;
171
+ rollbacks_in_window: number;
172
+ window_turns: number;
173
+ } | {
174
+ kind: "entropy_alert";
175
+ turn: number;
176
+ score: number;
177
+ threshold: number;
200
178
  } | {
201
179
  kind: "agent_process_changed";
202
180
  turn: number;
203
- category?: KernelEventCategory;
204
- primitive?: KernelPrimitive;
205
181
  agent_id: string;
206
182
  parent_session_id: string;
207
183
  role: string;
@@ -213,24 +189,18 @@ export type SessionEvent = {
213
189
  } | {
214
190
  kind: "memory_written";
215
191
  turn: number;
216
- category?: KernelEventCategory;
217
- primitive?: KernelPrimitive;
218
192
  memory_id: string;
219
193
  memory_kind: string;
220
194
  size_bytes: number;
221
195
  } | {
222
196
  kind: "memory_queried";
223
197
  turn: number;
224
- category?: KernelEventCategory;
225
- primitive?: KernelPrimitive;
226
198
  query_context: string;
227
199
  requested_k: number;
228
200
  requires_async_response: boolean;
229
201
  } | {
230
202
  kind: "memory_validation_failed";
231
203
  turn: number;
232
- category?: KernelEventCategory;
233
- primitive?: KernelPrimitive;
234
204
  memory_id: string;
235
205
  error: string;
236
206
  } | {
@@ -240,29 +210,35 @@ export type SessionEvent = {
240
210
  } | {
241
211
  kind: "workflow_node_completed";
242
212
  turn: number;
243
- category?: KernelEventCategory;
244
- primitive?: KernelPrimitive;
245
213
  agent_id: string;
246
214
  termination: string;
215
+ /** W-1: result-borne control signals, persisted so resume replays control flow faithfully —
216
+ * a classifier re-prunes its rejected branches, a recorded loop stop is honored. */
217
+ classify_branch?: string;
218
+ tournament_winner?: string;
219
+ loop_continue?: boolean;
220
+ /** W-1: the node's final output text — resume re-seeds the driver's outputs map from it so
221
+ * post-resume reduce/judge/dependent nodes still see their dependencies' outputs. */
222
+ output?: string;
247
223
  } | {
248
224
  kind: "workflow_nodes_submitted";
249
225
  turn: number;
250
- category?: KernelEventCategory;
251
- primitive?: KernelPrimitive;
252
226
  /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
253
227
  nodes: Record<string, unknown>[];
228
+ /** R3-1: graph base index the batch was appended at (from the kernel's
229
+ * WorkflowNodesSubmitted observation) — lets resume rebuild exact indices. */
230
+ base_index?: number;
231
+ /** W-N3: the submitting node's agent id (absent = host/bootstrap). Resume DROPS batches whose
232
+ * submitter re-runs — it will re-submit — instead of duplicating their nodes. */
233
+ submitter_agent_id?: string;
254
234
  } | {
255
235
  kind: "workflow_batch_spawned";
256
236
  turn: number;
257
- category?: KernelEventCategory;
258
- primitive?: KernelPrimitive;
259
237
  node_count: number;
260
238
  node_ids: string[];
261
239
  } | {
262
240
  kind: "workflow_completed";
263
241
  turn: number;
264
- category?: KernelEventCategory;
265
- primitive?: KernelPrimitive;
266
242
  completed: string[];
267
243
  failed: string[];
268
244
  total_nodes: number;
@@ -279,10 +255,26 @@ export type SessionEvent = {
279
255
  kind: "group_member_joined";
280
256
  session_id: string;
281
257
  role?: string;
258
+ member_kind?: "peer" | "vehicle";
282
259
  } | {
283
260
  kind: "group_budget_charged";
284
261
  tokens: number;
285
262
  subagents: number;
263
+ rounds?: number;
264
+ } | {
265
+ kind: "round_started";
266
+ /** 1-based round number within the loop. */
267
+ round: number;
268
+ goal: string;
269
+ } | {
270
+ kind: "round_paced";
271
+ round: number;
272
+ action: "continue" | "sleep" | "stop";
273
+ delay_ms?: number;
274
+ /** Absolute wake time for sleep — lets a stateless host re-arm from the log alone. */
275
+ wake_at_ms?: number;
276
+ reason: string;
277
+ coerced_from?: string;
286
278
  };
287
279
  export interface SessionLog {
288
280
  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
  }
@@ -62,14 +62,38 @@ function deriveMetaTools(permitted, opts) {
62
62
  metaTools.add("update_plan");
63
63
  return metaTools;
64
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
+ }
65
79
  /** Host-side driver for kernel-isolated sub-agent runs. */
66
80
  export class SubAgentOrchestrator {
67
- 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";
68
90
  const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
69
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
70
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
71
- // M3/G4: a worktree node runs inside its own git worktree (created here, removed in `finally`).
72
- 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);
73
97
  let systemPrompt = ctx.parentOpts.systemPrompt;
74
98
  let inheritEvents;
75
99
  if (ctx.manifest.context_inheritance === "full") {
@@ -102,10 +126,21 @@ export class SubAgentOrchestrator {
102
126
  enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
103
127
  // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
104
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,
105
136
  });
106
137
  // #2-B-ii: when the parent preempts this node (kernel `AgentPreempted`), interrupt the child —
107
138
  // cancelling its in-flight LLM call. Handle an already-aborted signal too (creation race).
108
139
  linkAbort(ctx.abortSignal, childRunner);
140
+ return { childRunner, inheritEvents, cleanupWorktree };
141
+ }
142
+ async *stream(ctx) {
143
+ const { childRunner, inheritEvents, cleanupWorktree } = await this.buildChild(ctx);
109
144
  try {
110
145
  yield* childRunner.run({
111
146
  sessionId: ctx.spec.identity.sessionId,
@@ -119,34 +154,12 @@ export class SubAgentOrchestrator {
119
154
  }
120
155
  async run(ctx) {
121
156
  if (ctx.harness) {
122
- const { RuntimeRunner } = await import("./runner.js");
123
157
  const { HarnessLoop } = await import("../harness/harness.js");
124
- const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
125
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
126
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
127
- // M3/G4: worktree isolation for a worktree node (cleaned up in `finally` below).
128
- const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, filteredPlane);
129
- const childRunner = new RuntimeRunner({
130
- ...ctx.parentOpts,
131
- // M1/G3: route to the node's hinted model (falls back to the parent provider).
132
- provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
133
- // M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
134
- maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
135
- // O3: per-child turn / wall-clock caps (fall back to the inherited limits).
136
- maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
137
- timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
138
- executionPlane: execPlane,
139
- agentId: ctx.spec.identity.agentId,
140
- sessionLog: ctx.sessionLog,
141
- skillDir: metaTools.has("skill") ? ctx.parentOpts.skillDir : undefined,
142
- dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
143
- knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
144
- enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
145
- // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
146
- isWorkflowNode: ctx.isWorkflowNode,
147
- });
148
- // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
149
- 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);
150
163
  const loop = new HarnessLoop(childRunner, ctx.harness.evalProvider, {
151
164
  maxAttempts: ctx.harness.maxAttempts ?? 3,
152
165
  });
@@ -194,6 +207,9 @@ export class SubAgentOrchestrator {
194
207
  turnsUsed: done?.iterations ?? 0,
195
208
  totalTokensUsed: done?.totalTokens ?? 0,
196
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 } : {}),
197
213
  };
198
214
  return {
199
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) {