@deepstrike/sdk 0.2.16 → 0.2.18

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,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;
@@ -85,6 +86,11 @@ export function subAgentResultToKernel(result) {
85
86
  : null,
86
87
  turns_used: result.result.turnsUsed,
87
88
  total_tokens_used: result.result.totalTokensUsed,
89
+ // A#2: control-flow signals — additive, omitted on the wire when unset so a plain spawn's
90
+ // result is byte-identical to before. The kernel reads each only for the matching node kind.
91
+ ...(result.result.loopContinue !== undefined ? { loop_continue: result.result.loopContinue } : {}),
92
+ ...(result.result.classifyBranch !== undefined ? { classify_branch: result.result.classifyBranch } : {}),
93
+ ...(result.result.tournamentWinner !== undefined ? { tournament_winner: result.result.tournamentWinner } : {}),
88
94
  },
89
95
  };
90
96
  }
@@ -107,22 +113,49 @@ export function workflowBudgetNote(budget) {
107
113
  if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
108
114
  parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
109
115
  }
116
+ if (budget.tokens_remaining != null && budget.tokens_max != null) {
117
+ parts.push(`tokens ${budget.tokens_used ?? 0}/${budget.tokens_max} used, ${budget.tokens_remaining} remaining`);
118
+ }
110
119
  if (parts.length === 0)
111
120
  return "";
112
121
  return (`[workflow budget] ${parts.join(" · ")}. ` +
113
- "If you submit more workflow nodes, keep the batch within the remaining node budget.");
122
+ "If you submit more workflow nodes, keep the batch within the remaining node and token budget.");
123
+ }
124
+ /** Normalize a `WorkflowTaskSpec` (object or bare goal string) to the kernel's `RuntimeTask` JSON. */
125
+ function workflowTaskToKernel(t) {
126
+ const task = typeof t === "string" ? { goal: t } : t;
127
+ return {
128
+ goal: task.goal,
129
+ // `criteria` is required by the kernel's RuntimeTask serde (no default).
130
+ criteria: task.criteria ?? [],
131
+ ...(task.lane ? { lane: task.lane } : {}),
132
+ };
133
+ }
134
+ /** Lower a node's control-flow kind to the kernel's serde-tagged `NodeKind` JSON, or `undefined` for
135
+ * a plain spawn. `reducer` / `loop` / `classify` / `tournament` are mutually exclusive — declaring
136
+ * more than one is a spec error (a node has exactly one kind). */
137
+ function nodeKindToKernel(n) {
138
+ const declared = [n.reducer != null, n.loop != null, n.classify != null, n.tournament != null].filter(Boolean).length;
139
+ if (declared > 1) {
140
+ throw new Error("a workflow node may declare at most one of: reducer, loop, classify, tournament");
141
+ }
142
+ if (n.reducer != null)
143
+ return { type: "reduce", reducer: n.reducer };
144
+ if (n.loop != null)
145
+ return { type: "loop", max_iters: n.loop.maxIters };
146
+ if (n.classify != null) {
147
+ return { type: "classify", branches: n.classify.branches.map(b => ({ label: b.label, nodes: b.nodes })) };
148
+ }
149
+ if (n.tournament != null)
150
+ return { type: "tournament", entrants: n.tournament.entrants.map(workflowTaskToKernel) };
151
+ return undefined;
114
152
  }
115
153
  /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` (the
116
154
  * whole spec) and `submit_workflow_nodes` (R3-1 runtime append) so the two encodings never drift. */
117
155
  export function workflowNodeSpecToKernel(n) {
118
- const task = typeof n.task === "string" ? { goal: n.task } : n.task;
156
+ const kind = nodeKindToKernel(n);
119
157
  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
- },
158
+ task: workflowTaskToKernel(n.task),
126
159
  role: n.role,
127
160
  // role/isolation/context_inheritance have no serde default in the kernel — always emit.
128
161
  isolation: n.isolation ?? "shared",
@@ -130,8 +163,10 @@ export function workflowNodeSpecToKernel(n) {
130
163
  ...(n.modelHint ? { model_hint: n.modelHint } : {}),
131
164
  ...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
132
165
  ...(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 } } : {}),
166
+ // A#2/G2: loop / classify / tournament / reduce lower to a serde-tagged `NodeKind`; spawn omits it.
167
+ ...(kind ? { kind } : {}),
168
+ // M4/G5: per-node token cap (additive; omitted when unset).
169
+ ...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
135
170
  ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
136
171
  };
137
172
  }
@@ -149,24 +184,94 @@ export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
149
184
  ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
150
185
  };
151
186
  }
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({
187
+ /** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
188
+ * `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
189
+ * `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
190
+ * on the flatten case (a quarantined author's nodes are coerced quarantined). */
191
+ export function submitWorkflowToKernel(spec, parentSessionId, submitterAgentId) {
192
+ return {
193
+ kind: "submit_workflow",
194
+ spec: workflowSpecToKernel(spec),
195
+ parent_session_id: parentSessionId,
196
+ ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
197
+ };
198
+ }
199
+ /** Shared JSON-Schema for a workflow-node batch (a DAG). Used by both `submit_workflow_nodes`
200
+ * (append) and `start_workflow` (M5 v1: author a sub-workflow), so the two tools never drift. */
201
+ const workflowNodesArraySchema = {
202
+ type: "array",
203
+ description: "Workflow nodes (a DAG); each runs as a gated sub-agent. A node may declare ONE control-flow kind " +
204
+ "— `loop` / `classify` / `tournament` / `reducer` — otherwise it is a plain spawn. `dependsOn` and " +
205
+ "`classify.branches[].nodes` are batch-relative (index 0 = this batch's first node).",
206
+ items: {
160
207
  type: "object",
161
208
  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? }.",
209
+ task: {
210
+ description: "The node's goal: a string, or an object { goal, criteria?, lane? }.",
211
+ oneOf: [
212
+ { type: "string" },
213
+ {
214
+ type: "object",
215
+ properties: {
216
+ goal: { type: "string" },
217
+ criteria: { type: "array", items: { type: "string" } },
218
+ },
219
+ required: ["goal"],
220
+ },
221
+ ],
222
+ },
223
+ role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
224
+ isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
225
+ contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
226
+ trust: { type: "string", enum: ["trusted", "quarantined"] },
227
+ outputSchema: {
228
+ type: "object",
229
+ description: "Optional JSON Schema the node's output must conform to (validated + retried SDK-side).",
230
+ },
231
+ modelHint: {
232
+ type: "string",
233
+ description: "Preferred model for this node (e.g. \"opus\"/\"sonnet\"/\"haiku\"); the host routes it.",
234
+ },
235
+ reducer: {
236
+ type: "string",
237
+ description: "Make this a deterministic reduce node (no LLM); names a registered reducer.",
238
+ },
239
+ loop: {
240
+ type: "object",
241
+ description: "Make this a loop node: re-run its agent up to maxIters times, ending early when it reports done.",
242
+ properties: { maxIters: { type: "integer", description: "Hard iteration cap." } },
243
+ required: ["maxIters"],
244
+ },
245
+ classify: {
246
+ type: "object",
247
+ description: "Make this a classify node: its agent picks one branch label; that branch's nodes run, the rest are pruned.",
248
+ properties: {
249
+ branches: {
250
+ type: "array",
251
+ items: {
252
+ type: "object",
253
+ properties: {
254
+ label: { type: "string" },
255
+ nodes: {
256
+ type: "array",
257
+ items: { type: "integer" },
258
+ description: "Batch-relative indices of the nodes to run when this branch is chosen.",
259
+ },
260
+ },
261
+ required: ["label", "nodes"],
262
+ },
263
+ },
264
+ },
265
+ required: ["branches"],
266
+ },
267
+ tournament: {
268
+ type: "object",
269
+ description: "Make this a tournament controller: generate each entrant, then pairwise-judge to one winner (this node's task is the criterion).",
270
+ properties: {
271
+ entrants: {
272
+ type: "array",
273
+ description: "≥2 candidate tasks to generate and judge.",
274
+ items: {
170
275
  oneOf: [
171
276
  { type: "string" },
172
277
  {
@@ -179,31 +284,63 @@ export const submitWorkflowNodesTool = {
179
284
  },
180
285
  ],
181
286
  },
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
287
  },
200
- required: ["task", "role"],
201
288
  },
289
+ required: ["entrants"],
290
+ },
291
+ tokenBudget: {
292
+ type: "integer",
293
+ description: "Cap this node's child run at this many cumulative tokens.",
294
+ },
295
+ dependsOn: {
296
+ type: "array",
297
+ items: { type: "integer" },
298
+ description: "Batch-relative, backward-only dependency indices within this submission.",
202
299
  },
203
300
  },
301
+ required: ["task", "role"],
302
+ },
303
+ };
304
+ /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
305
+ * (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
306
+ * the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
307
+ export const submitWorkflowNodesTool = {
308
+ name: "submit_workflow_nodes",
309
+ description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
310
+ "spawns as a gated sub-agent. Use when you discover more work that should run as its own node. " +
311
+ "A node may declare ONE control-flow kind — `loop` (re-run until done), `classify` (route to one " +
312
+ "branch), `tournament` (pairwise-judge candidates to a winner), or `reducer` (deterministic, no " +
313
+ "LLM) — otherwise it is a plain spawn. Within a submission, `dependsOn` and `classify.branches[].nodes` " +
314
+ "are batch-relative (index 0 = this batch's first node).",
315
+ parameters: JSON.stringify({
316
+ type: "object",
317
+ properties: { nodes: workflowNodesArraySchema },
204
318
  required: ["nodes"],
205
319
  }),
206
320
  };
321
+ /** M5 v1 (flatten): the tool an agent calls to **author a sub-workflow** — a cohesive DAG of nodes
322
+ * (incl. loop/classify/tournament/reduce) composed onto the running workflow. Mechanically it lowers
323
+ * to the same append path as `submit_workflow_nodes` (a `WorkflowSpec` is a node batch), but reads as
324
+ * "write a harness" rather than "append nodes". v2 adds top-level bootstrap (the `LoadWorkflow`
325
+ * kernel syscall) so a plain run can start a workflow from scratch. */
326
+ export const startWorkflowTool = {
327
+ name: "start_workflow",
328
+ description: "Author and run a sub-workflow: a DAG of nodes (fan-out / classify / tournament / loop / reduce) " +
329
+ "composed onto the current run. Use to structure a multi-step task as its own harness. The nodes " +
330
+ "spawn as gated sub-agents; `dependsOn` / `classify.branches[].nodes` are spec-relative.",
331
+ parameters: JSON.stringify({
332
+ type: "object",
333
+ properties: {
334
+ spec: {
335
+ type: "object",
336
+ description: "The workflow specification.",
337
+ properties: { nodes: workflowNodesArraySchema },
338
+ required: ["nodes"],
339
+ },
340
+ },
341
+ required: ["spec"],
342
+ }),
343
+ };
207
344
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
208
345
  export function workflowNodeToSpec(node, parentSessionId) {
209
346
  return {
@@ -216,6 +353,10 @@ export function workflowNodeToSpec(node, parentSessionId) {
216
353
  role: node.role,
217
354
  isolation: node.isolation,
218
355
  goal: node.goal,
356
+ // M1/G3: carry the node's model preference so the orchestrator can route to a provider.
357
+ ...(node.model_hint ? { modelHint: node.model_hint } : {}),
358
+ // M4/G5: carry the node's token cap so the orchestrator can bound the child run.
359
+ ...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
219
360
  };
220
361
  }
221
362
  /** Build the host manifest for a kernel-generated workflow node. */
@@ -269,6 +410,34 @@ export function generateAndFilter(generators, filter) {
269
410
  });
270
411
  return { nodes };
271
412
  }
413
+ /**
414
+ * Generate→evaluate quality gate (the EvalPipeline successor, #6): a `loop` worker node (re-run up
415
+ * to `maxIters`, stopping early on a `loop_continue=false` self-signal) + a bias-resistant `verify`
416
+ * eval node gated on it, carrying the kernel's verdict `outputSchema`. Mirrors the kernel `gen_eval`
417
+ * template. For the iterative retry-with-feedback variant, drive it with `HarnessLoop`.
418
+ */
419
+ export function genEval(worker, evaluate, maxIters = 3, extractSkillOnPass = true) {
420
+ const schema = JSON.parse(getKernel().verdictOutputSchema(extractSkillOnPass));
421
+ return {
422
+ nodes: [
423
+ {
424
+ task: asTask(worker),
425
+ role: "implement",
426
+ isolation: "worktree",
427
+ contextInheritance: "full",
428
+ loop: { maxIters: Math.max(1, maxIters) },
429
+ },
430
+ {
431
+ task: asTask(evaluate),
432
+ role: "verify",
433
+ isolation: "read_only",
434
+ contextInheritance: "none",
435
+ dependsOn: [0],
436
+ outputSchema: schema,
437
+ },
438
+ ],
439
+ };
440
+ }
272
441
  /**
273
442
  * One fresh-context verifier per rule/claim (parallel) + optional skeptic that depends on all and
274
443
  * re-checks flags. Verifiers run read-only with no inherited author context (bias-resistant).
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.16",
3
+ "version": "0.2.18",
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.16",
23
+ "@deepstrike/core": "0.2.18",
24
24
  "@google/generative-ai": "^0.24.1",
25
25
  "openai": "^5.23.2"
26
26
  },