@deepstrike/wasm 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.
@@ -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
  }
@@ -105,31 +110,58 @@ export function workflowBudgetNote(budget) {
105
110
  if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
106
111
  parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
107
112
  }
113
+ if (budget.tokens_remaining != null && budget.tokens_max != null) {
114
+ parts.push(`tokens ${budget.tokens_used ?? 0}/${budget.tokens_max} used, ${budget.tokens_remaining} remaining`);
115
+ }
108
116
  if (parts.length === 0)
109
117
  return "";
110
118
  return (`[workflow budget] ${parts.join(" · ")}. ` +
111
- "If you submit more workflow nodes, keep the batch within the remaining node budget.");
119
+ "If you submit more workflow nodes, keep the batch within the remaining node and token budget.");
120
+ }
121
+ /** Normalize a `WorkflowTaskSpec` (object or bare goal string) to the kernel's `RuntimeTask` JSON. */
122
+ function workflowTaskToKernel(t) {
123
+ const task = typeof t === "string" ? { goal: t } : t;
124
+ return {
125
+ goal: task.goal,
126
+ // `criteria` is required by the kernel's RuntimeTask serde (no default).
127
+ criteria: task.criteria ?? [],
128
+ ...(task.lane ? { lane: task.lane } : {}),
129
+ };
130
+ }
131
+ /** Lower a node's control-flow kind to the kernel's serde-tagged `NodeKind` JSON, or `undefined` for
132
+ * a plain spawn. `reducer` / `loop` / `classify` / `tournament` are mutually exclusive. */
133
+ function nodeKindToKernel(n) {
134
+ const declared = [n.reducer != null, n.loop != null, n.classify != null, n.tournament != null].filter(Boolean).length;
135
+ if (declared > 1) {
136
+ throw new Error("a workflow node may declare at most one of: reducer, loop, classify, tournament");
137
+ }
138
+ if (n.reducer != null)
139
+ return { type: "reduce", reducer: n.reducer };
140
+ if (n.loop != null)
141
+ return { type: "loop", max_iters: n.loop.maxIters };
142
+ if (n.classify != null) {
143
+ return { type: "classify", branches: n.classify.branches.map(b => ({ label: b.label, nodes: b.nodes })) };
144
+ }
145
+ if (n.tournament != null)
146
+ return { type: "tournament", entrants: n.tournament.entrants.map(workflowTaskToKernel) };
147
+ return undefined;
112
148
  }
113
- /** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
114
149
  /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
115
150
  * `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
116
151
  export function workflowNodeSpecToKernel(n) {
117
- const task = typeof n.task === "string" ? { goal: n.task } : n.task;
152
+ const kind = nodeKindToKernel(n);
118
153
  return {
119
- task: {
120
- goal: task.goal,
121
- // `criteria` is required by the kernel's RuntimeTask serde (no default).
122
- criteria: task.criteria ?? [],
123
- ...(task.lane ? { lane: task.lane } : {}),
124
- },
154
+ task: workflowTaskToKernel(n.task),
125
155
  role: n.role,
126
156
  isolation: n.isolation ?? "shared",
127
157
  context_inheritance: n.contextInheritance ?? "none",
128
158
  ...(n.modelHint ? { model_hint: n.modelHint } : {}),
129
159
  ...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
130
160
  ...(n.outputSchema ? { output_schema: n.outputSchema } : {}),
131
- // G2: a reducer name lowers to the kernel's `NodeKind::Reduce` (serde-tagged by `type`).
132
- ...(n.reducer ? { kind: { type: "reduce", reducer: n.reducer } } : {}),
161
+ // A#2/G2: loop / classify / tournament / reduce lower to a serde-tagged `NodeKind`; spawn omits it.
162
+ ...(kind ? { kind } : {}),
163
+ // M4/G5: per-node token cap (additive; omitted when unset).
164
+ ...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
133
165
  ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
134
166
  };
135
167
  }
@@ -146,33 +178,113 @@ export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
146
178
  ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
147
179
  };
148
180
  }
181
+ /** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
182
+ * `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
183
+ * `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
184
+ * on the flatten case. */
185
+ export function submitWorkflowToKernel(spec, parentSessionId, submitterAgentId) {
186
+ return {
187
+ kind: "submit_workflow",
188
+ spec: workflowSpecToKernel(spec),
189
+ parent_session_id: parentSessionId,
190
+ ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
191
+ };
192
+ }
149
193
  /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
194
+ /** Shared JSON-Schema for a workflow-node batch (a DAG). Used by both `submit_workflow_nodes`
195
+ * (append) and `start_workflow` (M5 v1: author a sub-workflow), so the two tools never drift. */
196
+ const workflowNodesArraySchema = {
197
+ type: "array",
198
+ items: {
199
+ type: "object",
200
+ properties: {
201
+ task: { description: "The node's goal: a string, or { goal, criteria?, lane? }." },
202
+ role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
203
+ isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
204
+ contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
205
+ trust: { type: "string", enum: ["trusted", "quarantined"] },
206
+ outputSchema: { type: "object", description: "Optional JSON Schema the node's output must conform to." },
207
+ modelHint: { type: "string", description: "Preferred model for this node (e.g. \"opus\"/\"sonnet\"); the host routes it." },
208
+ reducer: { type: "string", description: "Make this a deterministic reduce node (no LLM); names a registered reducer." },
209
+ loop: {
210
+ type: "object",
211
+ description: "Make this a loop node: re-run up to maxIters times, ending early when the agent reports done.",
212
+ properties: { maxIters: { type: "integer", description: "Hard iteration cap." } },
213
+ required: ["maxIters"],
214
+ },
215
+ classify: {
216
+ type: "object",
217
+ description: "Make this a classify node: pick one branch label; that branch's nodes run, the rest are pruned.",
218
+ properties: {
219
+ branches: {
220
+ type: "array",
221
+ items: {
222
+ type: "object",
223
+ properties: {
224
+ label: { type: "string" },
225
+ nodes: { type: "array", items: { type: "integer" }, description: "Batch-relative node indices for this branch." },
226
+ },
227
+ required: ["label", "nodes"],
228
+ },
229
+ },
230
+ },
231
+ required: ["branches"],
232
+ },
233
+ tournament: {
234
+ type: "object",
235
+ description: "Make this a tournament controller: generate each entrant, then pairwise-judge to one winner.",
236
+ properties: {
237
+ entrants: {
238
+ type: "array",
239
+ description: "≥2 candidate tasks to generate and judge.",
240
+ items: {
241
+ oneOf: [
242
+ { type: "string" },
243
+ { type: "object", properties: { goal: { type: "string" }, criteria: { type: "array", items: { type: "string" } } }, required: ["goal"] },
244
+ ],
245
+ },
246
+ },
247
+ },
248
+ required: ["entrants"],
249
+ },
250
+ tokenBudget: { type: "integer", description: "Cap this node's child run at this many cumulative tokens." },
251
+ dependsOn: { type: "array", items: { type: "integer" } },
252
+ },
253
+ required: ["task", "role"],
254
+ },
255
+ };
150
256
  export const submitWorkflowNodesTool = {
151
257
  name: "submit_workflow_nodes",
152
258
  description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
153
- "spawns as a gated sub-agent. Use when you discover more work that should run as its own node.",
259
+ "spawns as a gated sub-agent. Use when you discover more work that should run as its own node. " +
260
+ "A node may declare ONE control-flow kind — `loop` / `classify` / `tournament` / `reducer` — " +
261
+ "otherwise it is a plain spawn. Within a submission, `dependsOn` and `classify.branches[].nodes` " +
262
+ "are batch-relative (index 0 = this batch's first node).",
263
+ parameters: JSON.stringify({
264
+ type: "object",
265
+ properties: { nodes: workflowNodesArraySchema },
266
+ required: ["nodes"],
267
+ }),
268
+ };
269
+ /** M5 v1 (flatten): the tool an agent calls to author a sub-workflow — a cohesive DAG of nodes
270
+ * composed onto the running workflow. Lowers to the same append path as `submit_workflow_nodes`
271
+ * (a `WorkflowSpec` is a node batch). v2 adds top-level bootstrap (the `LoadWorkflow` syscall). */
272
+ export const startWorkflowTool = {
273
+ name: "start_workflow",
274
+ description: "Author and run a sub-workflow: a DAG of nodes (fan-out / classify / tournament / loop / reduce) " +
275
+ "composed onto the current run. Use to structure a multi-step task as its own harness. The nodes " +
276
+ "spawn as gated sub-agents; `dependsOn` / `classify.branches[].nodes` are spec-relative.",
154
277
  parameters: JSON.stringify({
155
278
  type: "object",
156
279
  properties: {
157
- nodes: {
158
- type: "array",
159
- items: {
160
- type: "object",
161
- properties: {
162
- task: { description: "The node's goal: a string, or { goal, criteria?, lane? }." },
163
- role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
164
- isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
165
- contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
166
- trust: { type: "string", enum: ["trusted", "quarantined"] },
167
- outputSchema: { type: "object", description: "Optional JSON Schema the node's output must conform to." },
168
- reducer: { type: "string", description: "Make this a deterministic reduce node (no LLM); names a registered reducer." },
169
- dependsOn: { type: "array", items: { type: "integer" } },
170
- },
171
- required: ["task", "role"],
172
- },
280
+ spec: {
281
+ type: "object",
282
+ description: "The workflow specification.",
283
+ properties: { nodes: workflowNodesArraySchema },
284
+ required: ["nodes"],
173
285
  },
174
286
  },
175
- required: ["nodes"],
287
+ required: ["spec"],
176
288
  }),
177
289
  };
178
290
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
@@ -187,6 +299,10 @@ export function workflowNodeToSpec(node, parentSessionId) {
187
299
  role: node.role,
188
300
  isolation: node.isolation,
189
301
  goal: node.goal,
302
+ // M1/G3: carry the node's model preference so the orchestrator can route to a provider.
303
+ ...(node.model_hint ? { modelHint: node.model_hint } : {}),
304
+ // M4/G5: carry the node's token cap so the orchestrator can bound the child run.
305
+ ...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
190
306
  };
191
307
  }
192
308
  /** Build the host manifest for a kernel-generated workflow node. */
@@ -0,0 +1,17 @@
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;
3
+ /** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
4
+ export declare function classifyInstruction(labels: string[]): string;
5
+ /** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */
6
+ export declare function judgeGoal(criterion: string, leftOutput: string, rightOutput: string): string;
7
+ /** Extract a loop stop signal from a loop iteration's output. Returns the `loopContinue` value, or
8
+ * `undefined` when the agent gave no clear signal (⇒ the kernel runs the loop to `max_iters`).
9
+ * Accepts `{loop_continue: bool}` or, leniently, `{done: bool}` (continue = !done). */
10
+ export declare function extractLoopContinue(text: string): boolean | undefined;
11
+ /** Extract the chosen branch label from a classifier's output. Prefers `{branch: "..."}`; falls back
12
+ * to a bare label string that exactly matches one of the valid labels. Returns `undefined` when no
13
+ * recognizable choice was made (the kernel then prunes every branch — a safe "none matched"). */
14
+ export declare function extractClassifyBranch(text: string, labels: string[]): string | undefined;
15
+ /** Extract a tournament judge's verdict ("left" or "right"). Defaults to "left" when the verdict is
16
+ * unparseable, so the bracket always advances to a champion rather than stalling with no winner. */
17
+ export declare function extractJudgeWinner(text: string): "left" | "right";
@@ -0,0 +1,78 @@
1
+ //! A#2: SDK-side execution of the kernel's control-flow workflow node kinds (Loop / Classify /
2
+ //! Tournament). The kernel owns the scheduling — it re-arms loops, prunes classify branches, and
3
+ //! runs the tournament bracket — and tells the SDK *which* kind a spawn is via the spawn descriptor
4
+ //! (`loop_max_iters` / `classify_labels` / `judge_match`). This module is the SDK half of the "one
5
+ //! agent per node + one additive result field" contract: it builds the prompt that solicits the
6
+ //! decision from the node's agent and extracts the matching result signal (`loopContinue` /
7
+ //! `classifyBranch` / `tournamentWinner`) the kernel reads back.
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}.`);
15
+ }
16
+ /** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
17
+ export function classifyInstruction(labels) {
18
+ return (`Classify the input and choose EXACTLY ONE label from: ${labels.map(l => JSON.stringify(l)).join(", ")}. ` +
19
+ `Respond with ONLY a JSON object: {"branch": "<one of the labels>"}.`);
20
+ }
21
+ /** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */
22
+ export function judgeGoal(criterion, leftOutput, rightOutput) {
23
+ return (`${criterion}\n\nCompare the two candidate outputs below and decide which one better satisfies the ` +
24
+ `criterion above.\n\n[CANDIDATE left]\n${leftOutput}\n\n[CANDIDATE right]\n${rightOutput}\n\n` +
25
+ `Respond with ONLY a JSON object: {"winner": "left"} or {"winner": "right"}.`);
26
+ }
27
+ /** Extract a loop stop signal from a loop iteration's output. Returns the `loopContinue` value, or
28
+ * `undefined` when the agent gave no clear signal (⇒ the kernel runs the loop to `max_iters`).
29
+ * Accepts `{loop_continue: bool}` or, leniently, `{done: bool}` (continue = !done). */
30
+ export function extractLoopContinue(text) {
31
+ const v = extractJsonValue(text);
32
+ if (v && typeof v === "object" && !Array.isArray(v)) {
33
+ const o = v;
34
+ if (typeof o.loop_continue === "boolean")
35
+ return o.loop_continue;
36
+ if (typeof o.loopContinue === "boolean")
37
+ return o.loopContinue;
38
+ if (typeof o.done === "boolean")
39
+ return !o.done;
40
+ }
41
+ return undefined;
42
+ }
43
+ /** Extract the chosen branch label from a classifier's output. Prefers `{branch: "..."}`; falls back
44
+ * to a bare label string that exactly matches one of the valid labels. Returns `undefined` when no
45
+ * recognizable choice was made (the kernel then prunes every branch — a safe "none matched"). */
46
+ export function extractClassifyBranch(text, labels) {
47
+ const v = extractJsonValue(text);
48
+ if (v && typeof v === "object" && !Array.isArray(v)) {
49
+ const o = v;
50
+ if (typeof o.branch === "string")
51
+ return o.branch;
52
+ if (typeof o.label === "string")
53
+ return o.label;
54
+ }
55
+ if (typeof v === "string" && labels.includes(v))
56
+ return v;
57
+ const trimmed = (text ?? "").trim();
58
+ if (labels.includes(trimmed))
59
+ return trimmed;
60
+ return undefined;
61
+ }
62
+ /** Extract a tournament judge's verdict ("left" or "right"). Defaults to "left" when the verdict is
63
+ * unparseable, so the bracket always advances to a champion rather than stalling with no winner. */
64
+ export function extractJudgeWinner(text) {
65
+ const v = extractJsonValue(text);
66
+ if (v && typeof v === "object" && !Array.isArray(v)) {
67
+ const w = v.winner;
68
+ if (w === "right")
69
+ return "right";
70
+ if (w === "left")
71
+ return "left";
72
+ }
73
+ const lowered = (text ?? "").toLowerCase();
74
+ // Last resort: a bare mention. Bias to "left" on ambiguity (both/neither mentioned).
75
+ if (lowered.includes("right") && !lowered.includes("left"))
76
+ return "right";
77
+ return "left";
78
+ }
@@ -1,9 +1,16 @@
1
1
  import type { ToolSchema, ToolResult } from "../types.js";
2
+ /** M3/G4: the runtime context a tool may read when executing (carries the working directory). A
3
+ * narrow, dependency-free shape; the execution plane's `RunContext` is structurally assignable to it.
4
+ * (WASM has no filesystem, so worktree isolation is N/A here — this keeps the tool ABI in parity
5
+ * with the Node/Python ports so a tool authored once works across all of them.) */
6
+ export interface ToolExecContext {
7
+ cwd?: string;
8
+ }
2
9
  export interface RegisteredTool {
3
10
  schema: ToolSchema;
4
- execute(args: Record<string, unknown>): Promise<string>;
11
+ execute(args: Record<string, unknown>, ctx?: ToolExecContext): Promise<string>;
5
12
  }
6
- export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>) => Promise<string> | string): RegisteredTool;
13
+ export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>, ctx?: ToolExecContext) => Promise<string> | string): RegisteredTool;
7
14
  export declare function executeTools(calls: {
8
15
  id: string;
9
16
  name: string;
@@ -1,7 +1,7 @@
1
1
  export function tool(name, description, parameters, fn) {
2
2
  return {
3
3
  schema: { name, description, parameters: JSON.stringify(parameters) },
4
- async execute(args) { return fn(args); },
4
+ async execute(args, ctx) { return fn(args, ctx); },
5
5
  };
6
6
  }
7
7
  export async function executeTools(calls, registry) {
package/dist/types.d.ts CHANGED
@@ -179,7 +179,11 @@ export interface LLMProvider {
179
179
  peekProviderReplay?(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
180
180
  seedProviderReplay?(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
181
181
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
182
- stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState): AsyncIterable<StreamEvent>;
182
+ stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState,
183
+ /** #2-B-ii: when provided, a preempt (`interrupt()`) aborts the in-flight request. SDK-client
184
+ * providers forward it via `{ signal }`; the runner also breaks the consume loop on abort, so
185
+ * providers that ignore it still stop processing immediately. Optional ⇒ backward-compatible. */
186
+ signal?: AbortSignal): AsyncIterable<StreamEvent>;
183
187
  }
184
188
  export interface DreamSummarizer {
185
189
  summarize(archived: Message[], context: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "node --experimental-vm-modules node_modules/.bin/jest"
16
16
  },
17
17
  "dependencies": {
18
- "@deepstrike/wasm-kernel": "0.2.16"
18
+ "@deepstrike/wasm-kernel": "0.2.18"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/jest": "^30.0.0",