@deepstrike/sdk 0.2.15 → 0.2.17

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.
@@ -85,6 +85,11 @@ export function subAgentResultToKernel(result) {
85
85
  : null,
86
86
  turns_used: result.result.turnsUsed,
87
87
  total_tokens_used: result.result.totalTokensUsed,
88
+ // A#2: control-flow signals — additive, omitted on the wire when unset so a plain spawn's
89
+ // result is byte-identical to before. The kernel reads each only for the matching node kind.
90
+ ...(result.result.loopContinue !== undefined ? { loop_continue: result.result.loopContinue } : {}),
91
+ ...(result.result.classifyBranch !== undefined ? { classify_branch: result.result.classifyBranch } : {}),
92
+ ...(result.result.tournamentWinner !== undefined ? { tournament_winner: result.result.tournamentWinner } : {}),
88
93
  },
89
94
  };
90
95
  }
@@ -107,22 +112,49 @@ export function workflowBudgetNote(budget) {
107
112
  if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
108
113
  parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
109
114
  }
115
+ if (budget.tokens_remaining != null && budget.tokens_max != null) {
116
+ parts.push(`tokens ${budget.tokens_used ?? 0}/${budget.tokens_max} used, ${budget.tokens_remaining} remaining`);
117
+ }
110
118
  if (parts.length === 0)
111
119
  return "";
112
120
  return (`[workflow budget] ${parts.join(" · ")}. ` +
113
- "If you submit more workflow nodes, keep the batch within the remaining node budget.");
121
+ "If you submit more workflow nodes, keep the batch within the remaining node and token budget.");
122
+ }
123
+ /** Normalize a `WorkflowTaskSpec` (object or bare goal string) to the kernel's `RuntimeTask` JSON. */
124
+ function workflowTaskToKernel(t) {
125
+ const task = typeof t === "string" ? { goal: t } : t;
126
+ return {
127
+ goal: task.goal,
128
+ // `criteria` is required by the kernel's RuntimeTask serde (no default).
129
+ criteria: task.criteria ?? [],
130
+ ...(task.lane ? { lane: task.lane } : {}),
131
+ };
132
+ }
133
+ /** Lower a node's control-flow kind to the kernel's serde-tagged `NodeKind` JSON, or `undefined` for
134
+ * a plain spawn. `reducer` / `loop` / `classify` / `tournament` are mutually exclusive — declaring
135
+ * more than one is a spec error (a node has exactly one kind). */
136
+ function nodeKindToKernel(n) {
137
+ const declared = [n.reducer != null, n.loop != null, n.classify != null, n.tournament != null].filter(Boolean).length;
138
+ if (declared > 1) {
139
+ throw new Error("a workflow node may declare at most one of: reducer, loop, classify, tournament");
140
+ }
141
+ if (n.reducer != null)
142
+ return { type: "reduce", reducer: n.reducer };
143
+ if (n.loop != null)
144
+ return { type: "loop", max_iters: n.loop.maxIters };
145
+ if (n.classify != null) {
146
+ return { type: "classify", branches: n.classify.branches.map(b => ({ label: b.label, nodes: b.nodes })) };
147
+ }
148
+ if (n.tournament != null)
149
+ return { type: "tournament", entrants: n.tournament.entrants.map(workflowTaskToKernel) };
150
+ return undefined;
114
151
  }
115
152
  /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` (the
116
153
  * whole spec) and `submit_workflow_nodes` (R3-1 runtime append) so the two encodings never drift. */
117
154
  export function workflowNodeSpecToKernel(n) {
118
- const task = typeof n.task === "string" ? { goal: n.task } : n.task;
155
+ const kind = nodeKindToKernel(n);
119
156
  return {
120
- task: {
121
- goal: task.goal,
122
- // `criteria` is required by the kernel's RuntimeTask serde (no default).
123
- criteria: task.criteria ?? [],
124
- ...(task.lane ? { lane: task.lane } : {}),
125
- },
157
+ task: workflowTaskToKernel(n.task),
126
158
  role: n.role,
127
159
  // role/isolation/context_inheritance have no serde default in the kernel — always emit.
128
160
  isolation: n.isolation ?? "shared",
@@ -130,8 +162,10 @@ export function workflowNodeSpecToKernel(n) {
130
162
  ...(n.modelHint ? { model_hint: n.modelHint } : {}),
131
163
  ...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
132
164
  ...(n.outputSchema ? { output_schema: n.outputSchema } : {}),
133
- // G2: a reducer name lowers to the kernel's `NodeKind::Reduce` (serde-tagged by `type`).
134
- ...(n.reducer ? { kind: { type: "reduce", reducer: n.reducer } } : {}),
165
+ // A#2/G2: loop / classify / tournament / reduce lower to a serde-tagged `NodeKind`; spawn omits it.
166
+ ...(kind ? { kind } : {}),
167
+ // M4/G5: per-node token cap (additive; omitted when unset).
168
+ ...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
135
169
  ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
136
170
  };
137
171
  }
@@ -149,24 +183,94 @@ export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
149
183
  ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
150
184
  };
151
185
  }
152
- /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
153
- * (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
154
- * the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
155
- export const submitWorkflowNodesTool = {
156
- name: "submit_workflow_nodes",
157
- description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
158
- "spawns as a gated sub-agent. Use when you discover more work that should run as its own node.",
159
- parameters: JSON.stringify({
186
+ /** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
187
+ * `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
188
+ * `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
189
+ * on the flatten case (a quarantined author's nodes are coerced quarantined). */
190
+ export function submitWorkflowToKernel(spec, parentSessionId, submitterAgentId) {
191
+ return {
192
+ kind: "submit_workflow",
193
+ spec: workflowSpecToKernel(spec),
194
+ parent_session_id: parentSessionId,
195
+ ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
196
+ };
197
+ }
198
+ /** Shared JSON-Schema for a workflow-node batch (a DAG). Used by both `submit_workflow_nodes`
199
+ * (append) and `start_workflow` (M5 v1: author a sub-workflow), so the two tools never drift. */
200
+ const workflowNodesArraySchema = {
201
+ type: "array",
202
+ description: "Workflow nodes (a DAG); each runs as a gated sub-agent. A node may declare ONE control-flow kind " +
203
+ "— `loop` / `classify` / `tournament` / `reducer` — otherwise it is a plain spawn. `dependsOn` and " +
204
+ "`classify.branches[].nodes` are batch-relative (index 0 = this batch's first node).",
205
+ items: {
160
206
  type: "object",
161
207
  properties: {
162
- nodes: {
163
- type: "array",
164
- description: "Workflow nodes to append; each runs as a gated sub-agent.",
165
- items: {
166
- type: "object",
167
- properties: {
168
- task: {
169
- description: "The node's goal: a string, or an object { goal, criteria?, lane? }.",
208
+ task: {
209
+ description: "The node's goal: a string, or an object { goal, criteria?, lane? }.",
210
+ oneOf: [
211
+ { type: "string" },
212
+ {
213
+ type: "object",
214
+ properties: {
215
+ goal: { type: "string" },
216
+ criteria: { type: "array", items: { type: "string" } },
217
+ },
218
+ required: ["goal"],
219
+ },
220
+ ],
221
+ },
222
+ role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
223
+ isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
224
+ contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
225
+ trust: { type: "string", enum: ["trusted", "quarantined"] },
226
+ outputSchema: {
227
+ type: "object",
228
+ description: "Optional JSON Schema the node's output must conform to (validated + retried SDK-side).",
229
+ },
230
+ modelHint: {
231
+ type: "string",
232
+ description: "Preferred model for this node (e.g. \"opus\"/\"sonnet\"/\"haiku\"); the host routes it.",
233
+ },
234
+ reducer: {
235
+ type: "string",
236
+ description: "Make this a deterministic reduce node (no LLM); names a registered reducer.",
237
+ },
238
+ loop: {
239
+ type: "object",
240
+ description: "Make this a loop node: re-run its agent up to maxIters times, ending early when it reports done.",
241
+ properties: { maxIters: { type: "integer", description: "Hard iteration cap." } },
242
+ required: ["maxIters"],
243
+ },
244
+ classify: {
245
+ type: "object",
246
+ description: "Make this a classify node: its agent picks one branch label; that branch's nodes run, the rest are pruned.",
247
+ properties: {
248
+ branches: {
249
+ type: "array",
250
+ items: {
251
+ type: "object",
252
+ properties: {
253
+ label: { type: "string" },
254
+ nodes: {
255
+ type: "array",
256
+ items: { type: "integer" },
257
+ description: "Batch-relative indices of the nodes to run when this branch is chosen.",
258
+ },
259
+ },
260
+ required: ["label", "nodes"],
261
+ },
262
+ },
263
+ },
264
+ required: ["branches"],
265
+ },
266
+ tournament: {
267
+ type: "object",
268
+ description: "Make this a tournament controller: generate each entrant, then pairwise-judge to one winner (this node's task is the criterion).",
269
+ properties: {
270
+ entrants: {
271
+ type: "array",
272
+ description: "≥2 candidate tasks to generate and judge.",
273
+ items: {
170
274
  oneOf: [
171
275
  { type: "string" },
172
276
  {
@@ -179,31 +283,63 @@ export const submitWorkflowNodesTool = {
179
283
  },
180
284
  ],
181
285
  },
182
- role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
183
- isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
184
- contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
185
- trust: { type: "string", enum: ["trusted", "quarantined"] },
186
- outputSchema: {
187
- type: "object",
188
- description: "Optional JSON Schema the node's output must conform to (validated + retried SDK-side).",
189
- },
190
- reducer: {
191
- type: "string",
192
- description: "Make this a deterministic reduce node (no LLM); names a registered reducer.",
193
- },
194
- dependsOn: {
195
- type: "array",
196
- items: { type: "integer" },
197
- description: "Batch-relative, backward-only dependency indices within this submission.",
198
- },
199
286
  },
200
- required: ["task", "role"],
201
287
  },
288
+ required: ["entrants"],
289
+ },
290
+ tokenBudget: {
291
+ type: "integer",
292
+ description: "Cap this node's child run at this many cumulative tokens.",
293
+ },
294
+ dependsOn: {
295
+ type: "array",
296
+ items: { type: "integer" },
297
+ description: "Batch-relative, backward-only dependency indices within this submission.",
202
298
  },
203
299
  },
300
+ required: ["task", "role"],
301
+ },
302
+ };
303
+ /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
304
+ * (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
305
+ * the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
306
+ export const submitWorkflowNodesTool = {
307
+ name: "submit_workflow_nodes",
308
+ description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
309
+ "spawns as a gated sub-agent. Use when you discover more work that should run as its own node. " +
310
+ "A node may declare ONE control-flow kind — `loop` (re-run until done), `classify` (route to one " +
311
+ "branch), `tournament` (pairwise-judge candidates to a winner), or `reducer` (deterministic, no " +
312
+ "LLM) — otherwise it is a plain spawn. Within a submission, `dependsOn` and `classify.branches[].nodes` " +
313
+ "are batch-relative (index 0 = this batch's first node).",
314
+ parameters: JSON.stringify({
315
+ type: "object",
316
+ properties: { nodes: workflowNodesArraySchema },
204
317
  required: ["nodes"],
205
318
  }),
206
319
  };
320
+ /** M5 v1 (flatten): the tool an agent calls to **author a sub-workflow** — a cohesive DAG of nodes
321
+ * (incl. loop/classify/tournament/reduce) composed onto the running workflow. Mechanically it lowers
322
+ * to the same append path as `submit_workflow_nodes` (a `WorkflowSpec` is a node batch), but reads as
323
+ * "write a harness" rather than "append nodes". v2 adds top-level bootstrap (the `LoadWorkflow`
324
+ * kernel syscall) so a plain run can start a workflow from scratch. */
325
+ export const startWorkflowTool = {
326
+ name: "start_workflow",
327
+ description: "Author and run a sub-workflow: a DAG of nodes (fan-out / classify / tournament / loop / reduce) " +
328
+ "composed onto the current run. Use to structure a multi-step task as its own harness. The nodes " +
329
+ "spawn as gated sub-agents; `dependsOn` / `classify.branches[].nodes` are spec-relative.",
330
+ parameters: JSON.stringify({
331
+ type: "object",
332
+ properties: {
333
+ spec: {
334
+ type: "object",
335
+ description: "The workflow specification.",
336
+ properties: { nodes: workflowNodesArraySchema },
337
+ required: ["nodes"],
338
+ },
339
+ },
340
+ required: ["spec"],
341
+ }),
342
+ };
207
343
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
208
344
  export function workflowNodeToSpec(node, parentSessionId) {
209
345
  return {
@@ -216,6 +352,10 @@ export function workflowNodeToSpec(node, parentSessionId) {
216
352
  role: node.role,
217
353
  isolation: node.isolation,
218
354
  goal: node.goal,
355
+ // M1/G3: carry the node's model preference so the orchestrator can route to a provider.
356
+ ...(node.model_hint ? { modelHint: node.model_hint } : {}),
357
+ // M4/G5: carry the node's token cap so the orchestrator can bound the child run.
358
+ ...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
219
359
  };
220
360
  }
221
361
  /** Build the host manifest for a kernel-generated workflow node. */
package/dist/types.d.ts CHANGED
@@ -304,7 +304,12 @@ export interface LLMProvider {
304
304
  */
305
305
  assessReplayability?(context: RenderedContext, extensions?: Record<string, unknown>): ReplayabilityAssessment;
306
306
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
307
- stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState): AsyncIterable<StreamEvent>;
307
+ stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState,
308
+ /** #2-B-ii: when provided, a preempting `InterruptNow` (or `interrupt()`) aborts the in-flight
309
+ * request. SDK-client providers should forward it to the client (`{ signal }`); the runner also
310
+ * breaks the consume loop on abort, so providers that ignore it still stop processing immediately
311
+ * (only the socket lingers). Optional ⇒ backward-compatible; providers may ignore it. */
312
+ signal?: AbortSignal): AsyncIterable<StreamEvent>;
308
313
  }
309
314
  /**
310
315
  * Optional async summarizer called after context compression.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "DeepStrike Node.js SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@anthropic-ai/sdk": "^0.99.0",
23
- "@deepstrike/core": "0.2.15",
23
+ "@deepstrike/core": "0.2.17",
24
24
  "@google/generative-ai": "^0.24.1",
25
25
  "openai": "^5.23.2"
26
26
  },