@deepstrike/wasm 0.2.36 → 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.
@@ -10,6 +10,13 @@ export interface SubAgentRunContext {
10
10
  /** M5 v2.1: set when this child is a workflow node — propagated so a nested `start_workflow`
11
11
  * FLATTENS to the parent kernel rather than auto-pivoting into its own bootstrap. */
12
12
  isWorkflowNode?: boolean;
13
+ /** W-N1 tool exposure. The kernel omits an EMPTY `permitted_capability_ids` on the wire, so a
14
+ * grant-less workflow node is indistinguishable from a zero-cap spawn at the manifest — the
15
+ * caller states intent instead: `"inherit"` = run on the parent's execution plane with its
16
+ * meta-tool availability (trusted workflow nodes — they carried no grant list by design, and
17
+ * filtering on the missing list ran every DAG node TOOL-LESS); `"filtered"` (default) = filter
18
+ * to the manifest grants, empty ⇒ deny-all (spawn path, quarantined nodes). */
19
+ toolAccess?: "inherit" | "filtered";
13
20
  /** #2-B-ii: parent-controlled abort — when the kernel preempts this node (`AgentPreempted`), the
14
21
  * orchestrator interrupts the child runner, cancelling its in-flight LLM call. */
15
22
  abortSignal?: AbortSignal;
@@ -49,12 +49,32 @@ function deriveMetaTools(permitted, opts) {
49
49
  metaTools.add("update_plan");
50
50
  return metaTools;
51
51
  }
52
+ /** W-N1: meta-tools by source availability alone — a `toolAccess: "inherit"` child gets the same
53
+ * meta surface a top-level run of these options would. */
54
+ function availableMetaTools(opts) {
55
+ const metaTools = new Set();
56
+ if (opts.skillContentMap?.size)
57
+ metaTools.add("skill");
58
+ if (opts.dreamStore)
59
+ metaTools.add("memory");
60
+ if (opts.knowledgeSource)
61
+ metaTools.add("knowledge");
62
+ if (opts.enablePlanTool)
63
+ metaTools.add("update_plan");
64
+ return metaTools;
65
+ }
52
66
  /** Host-side driver for kernel-isolated sub-agent runs. */
53
67
  export class SubAgentOrchestrator {
54
68
  async run(ctx) {
69
+ // W-N1: "inherit" runs the child on the parent's plane with availability-derived meta-tools
70
+ // (trusted workflow nodes); "filtered" (default) filters to the manifest grants, empty ⇒
71
+ // deny-all (spawn path, quarantined nodes).
72
+ const inherit = ctx.toolAccess === "inherit";
55
73
  const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
56
- const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
57
- const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
74
+ const metaTools = inherit ? availableMetaTools(ctx.parentOpts) : deriveMetaTools(permitted, ctx.parentOpts);
75
+ const execPlane = inherit
76
+ ? ctx.parentOpts.executionPlane
77
+ : new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
58
78
  let systemPrompt = ctx.parentOpts.systemPrompt;
59
79
  let inheritEvents;
60
80
  if (ctx.manifest.context_inheritance === "full") {
@@ -77,7 +97,7 @@ export class SubAgentOrchestrator {
77
97
  // O3: per-child turn / wall-clock caps (fall back to the inherited limits).
78
98
  maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
79
99
  timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
80
- executionPlane: filteredPlane,
100
+ executionPlane: execPlane,
81
101
  agentId: ctx.spec.identity.agentId,
82
102
  systemPrompt,
83
103
  sessionLog: ctx.sessionLog,
@@ -87,6 +107,13 @@ export class SubAgentOrchestrator {
87
107
  enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
88
108
  // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
89
109
  isWorkflowNode: ctx.isWorkflowNode,
110
+ // The child runs under ITS OWN spec, never the parent's: the spread above would otherwise
111
+ // leak the parent's `runSpec` (identity, capability filter — and an armed `loopRound`,
112
+ // giving every child a phantom pace tool). A loop-node iteration carries its own minimal
113
+ // spec to arm the pacing trap (DW-3); everything else runs spec-less as before.
114
+ runSpec: ctx.spec.loopRound
115
+ ? { identity: ctx.spec.identity, role: ctx.spec.role, goal: ctx.spec.goal, loopRound: ctx.spec.loopRound }
116
+ : undefined,
90
117
  });
91
118
  // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
92
119
  linkAbort(ctx.abortSignal, childRunner);
@@ -112,6 +139,9 @@ export class SubAgentOrchestrator {
112
139
  ...(finalText
113
140
  ? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } }
114
141
  : {}),
142
+ // DW-3: surface the kernel-adjudicated pace decision (loop-node iterations consume it as the
143
+ // continuation vocabulary). SDK-internal; stripped by subAgentResultToKernel.
144
+ ...(done?.paceDecision ? { paceDecision: done.paceDecision } : {}),
115
145
  };
116
146
  return {
117
147
  agentId: ctx.spec.identity.agentId,
@@ -19,6 +19,16 @@ export interface AgentCapabilityFilter {
19
19
  allowedKinds?: string[];
20
20
  allowedIds?: string[];
21
21
  }
22
+ export interface LoopRoundSpec {
23
+ /** Hard round cap across the loop's lifetime; continue/sleep at the cap is coerced to stop. */
24
+ maxRounds?: number;
25
+ /** Sleep clamp floor (ms). */
26
+ minSleepMs?: number;
27
+ /** Sleep clamp ceiling (ms). */
28
+ maxSleepMs?: number;
29
+ /** Fallback when a round ends without a pace call: "stop" (goal loops, default) | "sleep" (cron loops). */
30
+ defaultAction?: "stop" | "sleep";
31
+ }
22
32
  export interface AgentRunSpec {
23
33
  identity: AgentIdentity;
24
34
  role: KernelAgentRole;
@@ -28,6 +38,9 @@ export interface AgentRunSpec {
28
38
  capabilityFilter?: AgentCapabilityFilter;
29
39
  milestones?: MilestoneContract;
30
40
  metadata?: Record<string, unknown>;
41
+ /** ③ loop-agent rounds: presence makes this run ONE round of a paced loop (gates the
42
+ * kernel `pace` meta-tool and arms the pacing trap). */
43
+ loopRound?: LoopRoundSpec;
31
44
  /** M1/G3: per-agent model preference; the host resolves it via `RuntimeOptions.providerFor`.
32
45
  * Host-side routing only — not sent to the kernel. */
33
46
  modelHint?: string;
@@ -70,6 +83,11 @@ export interface LoopResult {
70
83
  classifyBranch?: string;
71
84
  /** A#2 tournament verdict: a judge reports the winning entrant's agent id here. Sent only when set. */
72
85
  tournamentWinner?: string;
86
+ /** ③ loop-agent pacing: the kernel-adjudicated after-round decision, surfaced by the orchestrator
87
+ * from the child's done event. For a loop-node iteration this is the PRIMARY continuation
88
+ * vocabulary (stop → loopContinue=false); the legacy text-sniffed signal is the fallback.
89
+ * SDK-internal — stripped by `subAgentResultToKernel`. */
90
+ paceDecision?: import("../kernel-step.js").PaceDecision;
73
91
  }
74
92
  export interface SubAgentResult {
75
93
  agentId: string;
@@ -141,6 +159,10 @@ export interface WorkflowNodeSpec {
141
159
  };
142
160
  /** M4/G5: cap this node's child run at `tokenBudget` cumulative tokens (the per-node "use N tokens"). */
143
161
  tokenBudget?: number;
162
+ /** O3: cap this node's child run at `maxTurns` provider turns (falls back to the parent's). */
163
+ maxTurns?: number;
164
+ /** O3: cap this node's child run at `maxWallMs` wall-clock milliseconds. */
165
+ maxWallMs?: number;
144
166
  /** Indices of nodes this node depends on. */
145
167
  dependsOn?: number[];
146
168
  }
@@ -156,11 +178,14 @@ export interface WorkflowSpawnInfo {
156
178
  isolation: string;
157
179
  context_inheritance: string;
158
180
  model_hint?: string;
181
+ /** W3 trust level: `"trusted"` | `"quarantined"`. */
182
+ trust?: string;
159
183
  /** G3: JSON Schema the node's output must conform to (carried verbatim from the spec). */
160
184
  output_schema?: Record<string, unknown>;
161
185
  /** G2: for a reduce node, the registered reducer name to run (no LLM). */
162
186
  reducer?: string;
163
- /** G2: the dependency agent ids whose outputs a reduce node consumes. */
187
+ /** The dependency agent ids for EVERY dependent node (W-N2: a DAG edge carries data). A reduce
188
+ * node's registered function consumes them; every other node gets its deps' outputs in context. */
164
189
  input_agent_ids?: string[];
165
190
  /** A#2: present only for a tournament *judge* spawn — the two entrant agent ids whose produced
166
191
  * outputs this judge compares. The runner looks them up and reports the winner as `tournamentWinner`. */
@@ -176,6 +201,10 @@ export interface WorkflowSpawnInfo {
176
201
  classify_labels?: string[];
177
202
  /** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
178
203
  token_budget?: number;
204
+ /** O3: per-node turn cap → the child run's `maxTurns`. */
205
+ max_turns?: number;
206
+ /** O3: per-node wall-clock cap (ms) → the child run's timeout. */
207
+ max_wall_ms?: number;
179
208
  }
180
209
  /** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
181
210
  * `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
@@ -225,3 +254,9 @@ export declare function generateAndFilter(generators: WorkflowTaskSpec[], filter
225
254
  * re-checks flags. Verifiers run read-only with no inherited author context (bias-resistant).
226
255
  */
227
256
  export declare function verifyRules(rules: WorkflowTaskSpec[], skeptic?: WorkflowTaskSpec): WorkflowSpec;
257
+ /**
258
+ * Generate → evaluate loop template (parity with node/python `genEval`): a worker Loop node
259
+ * (worktree isolation, full context) feeding a fresh-context verifier whose output is pinned
260
+ * to the kernel's verdict schema. Async because the wasm kernel module loads lazily.
261
+ */
262
+ export declare function genEval(worker: WorkflowTaskSpec, evaluate: WorkflowTaskSpec, maxIters?: number, extractSkillOnPass?: boolean): Promise<WorkflowSpec>;
@@ -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;
@@ -45,6 +46,14 @@ export function agentRunSpecToKernel(spec) {
45
46
  out.verification_contract_id = spec.verificationContractId;
46
47
  if (spec.milestones)
47
48
  out.milestones = milestoneContractToKernel(spec.milestones);
49
+ if (spec.loopRound) {
50
+ out.loop_round = {
51
+ ...(spec.loopRound.maxRounds !== undefined ? { max_rounds: spec.loopRound.maxRounds } : {}),
52
+ ...(spec.loopRound.minSleepMs !== undefined ? { min_sleep_ms: spec.loopRound.minSleepMs } : {}),
53
+ ...(spec.loopRound.maxSleepMs !== undefined ? { max_sleep_ms: spec.loopRound.maxSleepMs } : {}),
54
+ ...(spec.loopRound.defaultAction !== undefined ? { default_action: spec.loopRound.defaultAction } : {}),
55
+ };
56
+ }
48
57
  return out;
49
58
  }
50
59
  export function milestoneContractToKernel(contract) {
@@ -175,6 +184,9 @@ export function workflowNodeSpecToKernel(n) {
175
184
  ...(kind ? { kind } : {}),
176
185
  // M4/G5: per-node token cap (additive; omitted when unset).
177
186
  ...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
187
+ // O3: per-node turn / wall-clock caps (additive; omitted when unset).
188
+ ...(n.maxTurns != null ? { max_turns: n.maxTurns } : {}),
189
+ ...(n.maxWallMs != null ? { max_wall_ms: n.maxWallMs } : {}),
178
190
  ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
179
191
  };
180
192
  }
@@ -302,10 +314,15 @@ export const startWorkflowTool = {
302
314
  };
303
315
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
304
316
  export function workflowNodeToSpec(node, parentSessionId) {
317
+ // W-N6 transcript-as-carry: a loop node's iterations share ONE stable session id (the `-i{k}`
318
+ // suffix names the spawn, not the session), so iteration k replays the transcript of 0..k-1 —
319
+ // "do the next increment" actually sees the previous increments. The agent_id keeps the
320
+ // per-iteration suffix (kernel completion routing).
321
+ const sessionNodeId = node.loop_max_iters != null ? node.agent_id.replace(/-i\d+$/, "") : node.agent_id;
305
322
  return {
306
323
  identity: {
307
324
  agentId: node.agent_id,
308
- sessionId: `${parentSessionId}-${node.agent_id}`,
325
+ sessionId: `${parentSessionId}-${sessionNodeId}`,
309
326
  isSubAgent: true,
310
327
  parentSessionId,
311
328
  },
@@ -316,6 +333,14 @@ export function workflowNodeToSpec(node, parentSessionId) {
316
333
  ...(node.model_hint ? { modelHint: node.model_hint } : {}),
317
334
  // M4/G5: carry the node's token cap so the orchestrator can bound the child run.
318
335
  ...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
336
+ // O3: carry the node's turn / wall-clock caps (the orchestrator already honors these).
337
+ ...(node.max_turns != null ? { maxTurns: node.max_turns } : {}),
338
+ ...(node.max_wall_ms != null ? { maxWallMs: node.max_wall_ms } : {}),
339
+ // DW-3 one continuation vocabulary: a loop ITERATION runs with the pacing trap armed, so the
340
+ // agent signals continue/stop through the kernel-adjudicated `pace` meta-tool instead of a
341
+ // text-sniffed JSON blob. One iteration = one round; the DAG (not max_rounds) caps iterations,
342
+ // and default stop means "ended without pacing" = done — the CC silence-is-completion contract.
343
+ ...(node.loop_max_iters != null ? { loopRound: { defaultAction: "stop" } } : {}),
319
344
  };
320
345
  }
321
346
  /** Build the host manifest for a kernel-generated workflow node. */
@@ -391,3 +416,31 @@ export function verifyRules(rules, skeptic) {
391
416
  }
392
417
  return { nodes };
393
418
  }
419
+ /**
420
+ * Generate → evaluate loop template (parity with node/python `genEval`): a worker Loop node
421
+ * (worktree isolation, full context) feeding a fresh-context verifier whose output is pinned
422
+ * to the kernel's verdict schema. Async because the wasm kernel module loads lazily.
423
+ */
424
+ export async function genEval(worker, evaluate, maxIters = 3, extractSkillOnPass = true) {
425
+ const kernel = await getKernel();
426
+ const schema = JSON.parse(kernel.verdictOutputSchema(extractSkillOnPass));
427
+ return {
428
+ nodes: [
429
+ {
430
+ task: asTask(worker),
431
+ role: "implement",
432
+ isolation: "worktree",
433
+ contextInheritance: "full",
434
+ loop: { maxIters: Math.max(1, maxIters) },
435
+ },
436
+ {
437
+ task: asTask(evaluate),
438
+ role: "verify",
439
+ isolation: "read_only",
440
+ contextInheritance: "none",
441
+ dependsOn: [0],
442
+ outputSchema: schema,
443
+ },
444
+ ],
445
+ };
446
+ }
@@ -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) {
package/dist/types.d.ts CHANGED
@@ -105,7 +105,8 @@ export interface DoneEvent extends StreamEvent {
105
105
  iterations: number;
106
106
  totalTokens: number;
107
107
  status: string;
108
- dreamResult?: import("./memory/index.js").DreamResult;
108
+ dreamResult?: import("./memory/index.js").DreamResult; /** ③ loop-agent: the kernel-adjudicated after-round decision (absent on non-loop runs). */
109
+ paceDecision?: import("./runtime/kernel-step.js").PaceDecision;
109
110
  }
110
111
  export interface ErrorEvent extends StreamEvent {
111
112
  type: "error";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
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.36"
18
+ "@deepstrike/wasm-kernel": "0.2.37"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/jest": "^30.0.0",