@deepstrike/wasm 0.2.9 → 0.2.11

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.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
4
4
  export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
5
5
  export type { SubAgentRunContext } from "./runtime/sub-agent-orchestrator.js";
6
6
  export type { AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRunSpec, AgentProcessChangedObservation, ContextInheritance, KernelAgentRole, LoopResult, MilestoneCheckResult, MilestoneContract, MilestonePhase, MilestonePolicy, SubAgentResult, TerminationReason, WorkflowSpec, WorkflowNodeSpec, WorkflowTaskSpec, WorkflowSpawnInfo, } from "./runtime/types/agent.js";
7
- export { workflowSpecToKernel, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
7
+ export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
8
8
  export { Governance } from "./governance.js";
9
9
  export type { GovernanceVerdict } from "./governance.js";
10
10
  export { AnthropicProvider } from "./providers/anthropic.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { RuntimeRunner, collectText, InMemorySessionLog, LocalExecutionPlane, DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./runtime/index.js";
2
2
  export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
3
3
  export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
4
- export { workflowSpecToKernel, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
4
+ export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
5
5
  export { Governance } from "./governance.js";
6
6
  export { AnthropicProvider } from "./providers/anthropic.js";
7
7
  export { OpenAIProvider, QwenProvider, DeepSeekProvider, MiniMaxProvider, KimiProvider } from "./providers/openai.js";
@@ -4,6 +4,8 @@ export type { RunContext, ExecutionPlane } from "./execution-plane.js";
4
4
  export { LocalExecutionPlane } from "./execution-plane.js";
5
5
  export type { MemoryPolicy, MemoryWriteRateLimit, ResourceQuota, RuntimeOptions, SchedulerBudget } from "./runner.js";
6
6
  export { RuntimeRunner, collectText } from "./runner.js";
7
+ export { builtinReducers, resolveReducer } from "./reducers.js";
8
+ export type { Reducer, ReducerRegistry, ReducerInput } from "./reducers.js";
7
9
  export { getKernel } from "./kernel.js";
8
10
  export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./os-profile.js";
9
11
  export type { NativeOsProfile, OsProfileId } from "./os-profile.js";
@@ -1,5 +1,6 @@
1
1
  export { InMemorySessionLog } from "./session-log.js";
2
2
  export { LocalExecutionPlane } from "./execution-plane.js";
3
3
  export { RuntimeRunner, collectText } from "./runner.js";
4
+ export { builtinReducers, resolveReducer } from "./reducers.js";
4
5
  export { getKernel } from "./kernel.js";
5
6
  export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./os-profile.js";
@@ -0,0 +1,14 @@
1
+ export interface SchemaValidation {
2
+ ok: boolean;
3
+ errors: string[];
4
+ }
5
+ type JsonSchema = Record<string, unknown>;
6
+ /** Validate `value` against `schema` (the supported subset). `path` is for error messages. */
7
+ export declare function validateAgainstSchema(value: unknown, schema: JsonSchema, path?: string): SchemaValidation;
8
+ /** The instruction appended to a node's goal so its agent produces schema-conforming JSON. */
9
+ export declare function schemaInstruction(schema: JsonSchema): string;
10
+ /** A stronger re-prompt for a retry after a validation failure. */
11
+ export declare function schemaRetryInstruction(schema: JsonSchema, errors: string[]): string;
12
+ /** Best-effort extraction of a JSON value from agent output (raw, fenced, or embedded). */
13
+ export declare function extractJsonValue(text: string): unknown;
14
+ export {};
@@ -0,0 +1,113 @@
1
+ // G3 structured output: a small, dependency-free JSON-Schema subset validator + helpers used by the
2
+ // workflow runner to enforce a node's `output_schema`. The kernel carries the schema verbatim (it is
3
+ // zero-I/O and never validates); enforcement lives here, SDK-side, where the agent output exists.
4
+ //
5
+ // Supported keywords (the common structured-output subset): `type` (object | array | string |
6
+ // number | integer | boolean | null), `required`, `properties` (recursive), `items` (recursive),
7
+ // `enum`. Unknown keywords are ignored rather than rejected — a permissive superset of these specs
8
+ // still validates, matching "instruct the model, then check the shape" rather than full JSON Schema.
9
+ function typeOfValue(v) {
10
+ if (v === null)
11
+ return "null";
12
+ if (Array.isArray(v))
13
+ return "array";
14
+ return typeof v; // "object" | "string" | "number" | "boolean"
15
+ }
16
+ function matchesType(v, t) {
17
+ switch (t) {
18
+ case "integer":
19
+ return typeof v === "number" && Number.isInteger(v);
20
+ case "number":
21
+ return typeof v === "number";
22
+ case "object":
23
+ return typeOfValue(v) === "object";
24
+ default:
25
+ return typeOfValue(v) === t;
26
+ }
27
+ }
28
+ /** Validate `value` against `schema` (the supported subset). `path` is for error messages. */
29
+ export function validateAgainstSchema(value, schema, path = "$") {
30
+ const errors = [];
31
+ const type = schema.type;
32
+ if (typeof type === "string" && !matchesType(value, type)) {
33
+ errors.push(`${path}: expected ${type}, got ${typeOfValue(value)}`);
34
+ return { ok: false, errors }; // type mismatch ⇒ stop; deeper checks are meaningless
35
+ }
36
+ if (Array.isArray(type) && !type.some(t => typeof t === "string" && matchesType(value, t))) {
37
+ errors.push(`${path}: expected one of [${type.join(", ")}], got ${typeOfValue(value)}`);
38
+ return { ok: false, errors };
39
+ }
40
+ if (Array.isArray(schema.enum) && !schema.enum.some(e => e === value)) {
41
+ errors.push(`${path}: value not in enum`);
42
+ }
43
+ if (typeOfValue(value) === "object") {
44
+ const obj = value;
45
+ const required = Array.isArray(schema.required) ? schema.required : [];
46
+ for (const key of required) {
47
+ if (!(key in obj))
48
+ errors.push(`${path}.${key}: required property missing`);
49
+ }
50
+ const properties = schema.properties ?? {};
51
+ for (const [key, sub] of Object.entries(properties)) {
52
+ if (key in obj) {
53
+ const r = validateAgainstSchema(obj[key], sub, `${path}.${key}`);
54
+ if (!r.ok)
55
+ errors.push(...r.errors);
56
+ }
57
+ }
58
+ }
59
+ if (typeOfValue(value) === "array" && schema.items && typeof schema.items === "object") {
60
+ const items = schema.items;
61
+ value.forEach((el, i) => {
62
+ const r = validateAgainstSchema(el, items, `${path}[${i}]`);
63
+ if (!r.ok)
64
+ errors.push(...r.errors);
65
+ });
66
+ }
67
+ return { ok: errors.length === 0, errors };
68
+ }
69
+ /** The instruction appended to a node's goal so its agent produces schema-conforming JSON. */
70
+ export function schemaInstruction(schema) {
71
+ return ("You MUST return ONLY a single JSON value that conforms to this JSON Schema, with no prose, " +
72
+ "no markdown, and no code fences:\n" +
73
+ JSON.stringify(schema));
74
+ }
75
+ /** A stronger re-prompt for a retry after a validation failure. */
76
+ export function schemaRetryInstruction(schema, errors) {
77
+ return (`${schemaInstruction(schema)}\n\nYour previous output did NOT conform: ${errors.join("; ")}. ` +
78
+ "Return ONLY the corrected JSON value.");
79
+ }
80
+ /** Best-effort extraction of a JSON value from agent output (raw, fenced, or embedded). */
81
+ export function extractJsonValue(text) {
82
+ const trimmed = (text ?? "").trim();
83
+ if (!trimmed)
84
+ return undefined;
85
+ const tryParse = (s) => {
86
+ try {
87
+ return JSON.parse(s);
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ };
93
+ const whole = tryParse(trimmed);
94
+ if (whole !== undefined)
95
+ return whole;
96
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
97
+ if (fence) {
98
+ const fenced = tryParse(fence[1].trim());
99
+ if (fenced !== undefined)
100
+ return fenced;
101
+ }
102
+ // Fall back to the first balanced {...} or [...] slice.
103
+ for (const [open, close] of [["{", "}"], ["[", "]"]]) {
104
+ const start = trimmed.indexOf(open);
105
+ const end = trimmed.lastIndexOf(close);
106
+ if (start !== -1 && end > start) {
107
+ const slice = tryParse(trimmed.slice(start, end + 1));
108
+ if (slice !== undefined)
109
+ return slice;
110
+ }
111
+ }
112
+ return undefined;
113
+ }
@@ -0,0 +1,15 @@
1
+ /** One dependency's contribution to a reduce: the producing node's agent id and its output text. */
2
+ export interface ReducerInput {
3
+ agentId: string;
4
+ output: string;
5
+ }
6
+ /** A pure function over a reduce node's dependency outputs → the reduce node's output string. */
7
+ export type Reducer = (inputs: ReducerInput[]) => string;
8
+ export type ReducerRegistry = Record<string, Reducer>;
9
+ /**
10
+ * Built-in reducers, available to every workflow without registration. A user-supplied registry is
11
+ * merged over these (so a custom reducer can shadow a built-in of the same name).
12
+ */
13
+ export declare const builtinReducers: ReducerRegistry;
14
+ /** Resolve a reducer by name from the built-ins overlaid with a user registry. */
15
+ export declare function resolveReducer(name: string, user?: ReducerRegistry): Reducer | undefined;
@@ -0,0 +1,57 @@
1
+ // G2 deterministic compute: the host-side reducer registry. A `NodeKind::Reduce` workflow node runs
2
+ // no LLM agent — the kernel hands the SDK a reducer name + its dependency outputs, and the SDK runs
3
+ // the named pure function here. This is the "ordinary code between stages" (dedupe / filter / merge /
4
+ // early-exit) of the code-orchestration model, expressed deterministically as a DAG node.
5
+ import { extractJsonValue } from "./output-schema.js";
6
+ /** Non-empty, trimmed lines of a string. */
7
+ function lines(s) {
8
+ return s
9
+ .split("\n")
10
+ .map(l => l.trim())
11
+ .filter(l => l.length > 0);
12
+ }
13
+ /**
14
+ * Built-in reducers, available to every workflow without registration. A user-supplied registry is
15
+ * merged over these (so a custom reducer can shadow a built-in of the same name).
16
+ */
17
+ export const builtinReducers = {
18
+ /** Concatenate every input's output, separated by blank lines, in dependency order. */
19
+ concat: inputs => inputs.map(i => i.output).join("\n\n"),
20
+ /** Union of non-empty lines across all inputs, first-seen order preserved (dedupe a fan-out). */
21
+ dedupe_lines: inputs => {
22
+ const seen = new Set();
23
+ const out = [];
24
+ for (const i of inputs) {
25
+ for (const line of lines(i.output)) {
26
+ if (!seen.has(line)) {
27
+ seen.add(line);
28
+ out.push(line);
29
+ }
30
+ }
31
+ }
32
+ return out.join("\n");
33
+ },
34
+ /** Parse each input as a JSON array, concatenate, dedupe by canonical JSON → a JSON array string. */
35
+ merge_json_arrays: inputs => {
36
+ const seen = new Set();
37
+ const merged = [];
38
+ for (const i of inputs) {
39
+ const v = extractJsonValue(i.output);
40
+ const arr = Array.isArray(v) ? v : v !== undefined ? [v] : [];
41
+ for (const el of arr) {
42
+ const key = JSON.stringify(el);
43
+ if (!seen.has(key)) {
44
+ seen.add(key);
45
+ merged.push(el);
46
+ }
47
+ }
48
+ }
49
+ return JSON.stringify(merged);
50
+ },
51
+ /** The number of inputs that produced any non-empty output — handy for early-exit/branch gates. */
52
+ count: inputs => String(inputs.filter(i => i.output.trim().length > 0).length),
53
+ };
54
+ /** Resolve a reducer by name from the built-ins overlaid with a user registry. */
55
+ export function resolveReducer(name, user) {
56
+ return user?.[name] ?? builtinReducers[name];
57
+ }
@@ -8,6 +8,7 @@ import type { ExecutionPlane } from "./execution-plane.js";
8
8
  import { type GovernancePolicy } from "../governance.js";
9
9
  import type { AgentRunSpec, SubAgentResult, MilestonePolicy, MilestoneContract, MilestoneCheckResult, WorkflowSpec } from "./types/agent.js";
10
10
  import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
11
+ import { type ReducerRegistry } from "./reducers.js";
11
12
  import { type NativeOsProfile, type OsProfileId } from "./os-profile.js";
12
13
  import { LargeResultSpool } from "./large-result-spool.js";
13
14
  export interface MemoryWriteRateLimit {
@@ -69,6 +70,8 @@ export interface RuntimeOptions {
69
70
  onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
70
71
  onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
71
72
  subAgentOrchestrator?: SubAgentOrchestrator;
73
+ /** G2: custom reducers for `NodeKind::Reduce` workflow nodes, merged over the built-ins. */
74
+ reducers?: ReducerRegistry;
72
75
  milestonePolicy?: MilestonePolicy;
73
76
  milestoneContract?: MilestoneContract;
74
77
  onMilestoneEvaluate?: (ctx: {
@@ -113,6 +116,19 @@ export declare class RuntimeRunner {
113
116
  dream(agentId: string, nowMs?: number): Promise<DreamResult>;
114
117
  private execute;
115
118
  spawnSubAgent(spec: AgentRunSpec): Promise<SubAgentResult>;
119
+ /**
120
+ * G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
121
+ * validating its output (the supported JSON-Schema subset), and re-running once with the errors
122
+ * fed back on mismatch. If it still does not conform, the node is failed with the validation
123
+ * reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
124
+ */
125
+ private runWorkflowNode;
126
+ /**
127
+ * G2: execute a deterministic reduce node — run the named reducer (built-ins overlaid with
128
+ * `opts.reducers`) over its dependency outputs and return a synthetic completion. No LLM, zero
129
+ * tokens. An unknown reducer or a thrown reducer fails the node (`Error` → starves dependents).
130
+ */
131
+ private runReduceNode;
116
132
  /**
117
133
  * W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
118
134
  * through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
@@ -120,6 +136,7 @@ export declare class RuntimeRunner {
120
136
  */
121
137
  runWorkflow(spec: WorkflowSpec, opts?: {
122
138
  resumedCompleted?: string[];
139
+ resumedSubmissions?: Record<string, unknown>[][];
123
140
  }): Promise<{
124
141
  completed: string[];
125
142
  failed: string[];
@@ -3,10 +3,12 @@ import { governancePolicyToKernelEvent } from "../governance.js";
3
3
  import { getKernel } from "./kernel.js";
4
4
  import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
5
5
  import { sanitizeReplayText } from "./replay-sanitize.js";
6
- import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, recoverCompletedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
6
+ import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
7
7
  import { forceCompact, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
8
- import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
8
+ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
9
9
  import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
10
+ import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
11
+ import { resolveReducer } from "./reducers.js";
10
12
  import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
11
13
  import { assertNativeProfile } from "./os-profile.js";
12
14
  import { LargeResultSpool } from "./large-result-spool.js";
@@ -527,7 +529,18 @@ export class RuntimeRunner {
527
529
  onPermissionRequest: this.opts.onPermissionRequest,
528
530
  };
529
531
  const toolResults = [];
530
- for await (const evt of this.opts.executionPlane.executeAll(allCalls, runCtx)) {
532
+ // R3-1: intercept `submit_workflow_nodes` it can't apply to this runner's kernel (when this
533
+ // runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
534
+ // the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
535
+ const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes");
536
+ const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes");
537
+ for (const call of submitCalls) {
538
+ const nodes = parseSubmitWorkflowNodesArgs(call.arguments);
539
+ yield { type: "workflow_nodes_submitted", nodes };
540
+ toolResults.push({ callId: call.id, output: "submitted", isError: false });
541
+ yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
542
+ }
543
+ for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
531
544
  yield evt;
532
545
  if (evt.type === "tool_result") {
533
546
  const tre = evt;
@@ -706,6 +719,81 @@ export class RuntimeRunner {
706
719
  });
707
720
  return result;
708
721
  }
722
+ /**
723
+ * G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
724
+ * validating its output (the supported JSON-Schema subset), and re-running once with the errors
725
+ * fed back on mismatch. If it still does not conform, the node is failed with the validation
726
+ * reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
727
+ */
728
+ async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
729
+ // G2: a reduce node runs no LLM — execute the registered pure function over its dependency
730
+ // outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
731
+ if (node.reducer) {
732
+ return this.runReduceNode(node, outputs ?? new Map());
733
+ }
734
+ const baseSpec = workflowNodeToSpec(node, parentSessionId);
735
+ const manifest = workflowNodeToManifest(node, parentSessionId);
736
+ // G4: surface remaining workflow budget so a coordinator node can size its submission.
737
+ const budgetNote = workflowBudgetNote(budget);
738
+ const withBudget = (goal) => (budgetNote ? `${goal}\n\n${budgetNote}` : goal);
739
+ const mkCtx = (goal) => ({
740
+ parentOpts: this.opts,
741
+ parentSessionId,
742
+ spec: { ...baseSpec, goal: withBudget(goal) },
743
+ manifest,
744
+ sessionLog: this.opts.sessionLog,
745
+ });
746
+ const schema = node.output_schema;
747
+ if (!schema)
748
+ return orchestrator.run(mkCtx(baseSpec.goal));
749
+ const MAX_ATTEMPTS = 2;
750
+ let last;
751
+ let lastErrors = [];
752
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
753
+ const goal = attempt === 1
754
+ ? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
755
+ : `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
756
+ const result = await orchestrator.run(mkCtx(goal));
757
+ const content = result.result.finalMessage?.content;
758
+ const text = typeof content === "string" ? content : content != null ? JSON.stringify(content) : "";
759
+ const v = validateAgainstSchema(extractJsonValue(text), schema);
760
+ if (v.ok)
761
+ return result;
762
+ last = result;
763
+ lastErrors = v.errors;
764
+ }
765
+ const reason = `output_schema validation failed after ${MAX_ATTEMPTS} attempts: ${lastErrors.join("; ")}`;
766
+ const fallback = last;
767
+ return {
768
+ ...fallback,
769
+ result: {
770
+ ...fallback.result,
771
+ termination: "error",
772
+ finalMessage: { role: "assistant", content: reason, toolCalls: [] },
773
+ },
774
+ };
775
+ }
776
+ /**
777
+ * G2: execute a deterministic reduce node — run the named reducer (built-ins overlaid with
778
+ * `opts.reducers`) over its dependency outputs and return a synthetic completion. No LLM, zero
779
+ * tokens. An unknown reducer or a thrown reducer fails the node (`Error` → starves dependents).
780
+ */
781
+ runReduceNode(node, outputs) {
782
+ const ok = (content, termination) => ({
783
+ agentId: node.agent_id,
784
+ result: { termination, finalMessage: { role: "assistant", content, toolCalls: [] }, turnsUsed: 0, totalTokensUsed: 0 },
785
+ });
786
+ const reducer = resolveReducer(node.reducer, this.opts.reducers);
787
+ if (!reducer)
788
+ return ok(`unknown reducer "${node.reducer}"`, "error");
789
+ const inputs = (node.input_agent_ids ?? []).map(agentId => ({ agentId, output: outputs.get(agentId) ?? "" }));
790
+ try {
791
+ return ok(reducer(inputs), "completed");
792
+ }
793
+ catch (err) {
794
+ return ok(`reducer "${node.reducer}" threw: ${err instanceof Error ? err.message : String(err)}`, "error");
795
+ }
796
+ }
709
797
  /**
710
798
  * W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
711
799
  * through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
@@ -718,34 +806,63 @@ export class RuntimeRunner {
718
806
  const parentSessionId = this.currentSessionId;
719
807
  const runtime = this.activeKernel;
720
808
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
721
- let observations = kernelApply(runtime, this.pendingObservations, {
809
+ const observations = kernelApply(runtime, this.pendingObservations, {
722
810
  kind: "load_workflow",
723
811
  spec: workflowSpecToKernel(spec),
724
812
  parent_session_id: parentSessionId,
725
813
  // W0-ABI resume: skip nodes already completed before an interruption.
726
814
  ...(opts?.resumedCompleted && opts.resumedCompleted.length ? { resumed_completed: opts.resumedCompleted } : {}),
815
+ // R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
816
+ ...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
727
817
  });
818
+ const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
819
+ // G4: the batch observation carries the workflow's remaining budget; track the latest.
820
+ const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
821
+ const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
822
+ let done = findDone(observations);
823
+ if (done)
824
+ return { completed: done.completed ?? [], failed: done.failed ?? [] };
825
+ let nodes = collectNodes(observations);
826
+ let budget = collectBudget(observations);
827
+ // G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
828
+ const outputs = new Map();
728
829
  for (;;) {
729
- const done = observations.find(o => o.kind === "workflow_completed");
730
- if (done)
731
- return { completed: done.completed ?? [], failed: done.failed ?? [] };
732
- const batch = observations.find(o => o.kind === "workflow_batch_spawned");
733
- const nodes = batch?.nodes ?? [];
734
830
  if (nodes.length === 0)
735
831
  return { completed: [], failed: [] };
736
- const results = await Promise.all(nodes.map(node => orchestrator.run({
737
- parentOpts: this.opts,
738
- parentSessionId,
739
- spec: workflowNodeToSpec(node, parentSessionId),
740
- manifest: workflowNodeToManifest(node, parentSessionId),
741
- sessionLog: this.opts.sessionLog,
742
- })));
743
- observations = [];
832
+ const roundBudget = budget;
833
+ const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs)));
834
+ // Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
835
+ const nextNodes = [];
836
+ done = undefined;
744
837
  for (const result of results) {
745
- observations = kernelApply(runtime, this.pendingObservations, {
838
+ // G2: record this node's output so a downstream reduce node can consume it.
839
+ const outContent = result.result.finalMessage?.content;
840
+ outputs.set(result.agentId, typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "");
841
+ // R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
842
+ // reporting the node's completion — the workflow is still active, so even a last-node
843
+ // submission keeps the DAG alive.
844
+ if (result.submittedNodes?.length) {
845
+ // G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
846
+ // nodes to quarantined (no topological privilege escalation).
847
+ const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
848
+ const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
849
+ nextNodes.push(...collectNodes(subObs));
850
+ budget = collectBudget(subObs) ?? budget;
851
+ // R3-1: persist the submission (kernel-shape nodes) so resume can re-apply it.
852
+ await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
853
+ turn: runtime.turn(),
854
+ nodes: submitEvent.nodes ?? [],
855
+ }));
856
+ }
857
+ const obs = kernelApply(runtime, this.pendingObservations, {
746
858
  kind: "sub_agent_completed",
747
859
  result: subAgentResultToKernel(result),
748
860
  });
861
+ nextNodes.push(...collectNodes(obs));
862
+ budget = collectBudget(obs) ?? budget;
863
+ const d = findDone(obs);
864
+ if (d)
865
+ done = d;
749
866
  // Persist node completion for resume recovery.
750
867
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
751
868
  turn: runtime.turn(),
@@ -753,6 +870,10 @@ export class RuntimeRunner {
753
870
  termination: result.result.termination,
754
871
  }));
755
872
  }
873
+ if (done && nextNodes.length === 0) {
874
+ return { completed: done.completed ?? [], failed: done.failed ?? [] };
875
+ }
876
+ nodes = nextNodes;
756
877
  }
757
878
  }
758
879
  /**
@@ -766,7 +887,8 @@ export class RuntimeRunner {
766
887
  }
767
888
  const events = await this.opts.sessionLog.read(this.currentSessionId);
768
889
  const resumedCompleted = recoverCompletedWorkflowNodes(events);
769
- return this.runWorkflow(spec, { resumedCompleted });
890
+ const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
891
+ return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions });
770
892
  }
771
893
  async appendObservations(sessionId, runtime, nextArchiveStart) {
772
894
  const turn = runtime.turn();
@@ -956,3 +1078,15 @@ export async function collectText(stream) {
956
1078
  }
957
1079
  return text;
958
1080
  }
1081
+ /** R3-1: parse `submit_workflow_nodes` tool args (`{ nodes: WorkflowNodeSpec[] }`). Node shapes are
1082
+ * trusted structurally; the kernel validates them on append. Malformed payload → no nodes. */
1083
+ function parseSubmitWorkflowNodesArgs(argsStr) {
1084
+ let parsed = {};
1085
+ try {
1086
+ parsed = JSON.parse(argsStr);
1087
+ }
1088
+ catch {
1089
+ // Ignore parse error → no nodes submitted.
1090
+ }
1091
+ return Array.isArray(parsed.nodes) ? parsed.nodes : [];
1092
+ }
@@ -239,6 +239,13 @@ export type SessionEvent = {
239
239
  primitive?: KernelPrimitive;
240
240
  agent_id: string;
241
241
  termination: string;
242
+ } | {
243
+ kind: "workflow_nodes_submitted";
244
+ turn: number;
245
+ category?: KernelEventCategory;
246
+ primitive?: KernelPrimitive;
247
+ /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
248
+ nodes: Record<string, unknown>[];
242
249
  } | {
243
250
  kind: "workflow_batch_spawned";
244
251
  turn: number;
@@ -52,3 +52,17 @@ export declare function recoverCompletedWorkflowNodes(events: Array<{
52
52
  seq: number;
53
53
  event: SessionEvent;
54
54
  }>): string[];
55
+ /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
56
+ * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
57
+ export declare function buildWorkflowNodesSubmittedEvent(input: {
58
+ turn: number;
59
+ nodes: Record<string, unknown>[];
60
+ }): Extract<SessionEvent, {
61
+ kind: "workflow_nodes_submitted";
62
+ }>;
63
+ /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
64
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
65
+ export declare function recoverSubmittedWorkflowNodes(events: Array<{
66
+ seq: number;
67
+ event: SessionEvent;
68
+ }>): Record<string, unknown>[][];
@@ -70,3 +70,18 @@ export function recoverCompletedWorkflowNodes(events) {
70
70
  }
71
71
  return completed;
72
72
  }
73
+ /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
74
+ * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
75
+ export function buildWorkflowNodesSubmittedEvent(input) {
76
+ return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
77
+ }
78
+ /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
79
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
80
+ export function recoverSubmittedWorkflowNodes(events) {
81
+ const submissions = [];
82
+ for (const { event } of events) {
83
+ if (event.kind === "workflow_nodes_submitted")
84
+ submissions.push(event.nodes);
85
+ }
86
+ return submissions;
87
+ }
@@ -41,6 +41,8 @@ export class SubAgentOrchestrator {
41
41
  });
42
42
  let done;
43
43
  let finalText = "";
44
+ // R3-1: collect any nodes this node's agent submitted via the `submit_workflow_nodes` tool.
45
+ const submittedNodes = [];
44
46
  for await (const evt of childRunner.run({
45
47
  sessionId: ctx.spec.identity.sessionId,
46
48
  goal: ctx.spec.goal,
@@ -49,6 +51,8 @@ export class SubAgentOrchestrator {
49
51
  finalText += evt.delta;
50
52
  if (evt.type === "done")
51
53
  done = evt;
54
+ if (evt.type === "workflow_nodes_submitted")
55
+ submittedNodes.push(...evt.nodes);
52
56
  }
53
57
  const loopResult = {
54
58
  termination: terminationFromStatus(done?.status ?? "error"),
@@ -58,7 +62,11 @@ export class SubAgentOrchestrator {
58
62
  ? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } }
59
63
  : {}),
60
64
  };
61
- return { agentId: ctx.spec.identity.agentId, result: loopResult };
65
+ return {
66
+ agentId: ctx.spec.identity.agentId,
67
+ result: loopResult,
68
+ ...(submittedNodes.length ? { submittedNodes } : {}),
69
+ };
62
70
  }
63
71
  }
64
72
  export const defaultSubAgentOrchestrator = new SubAgentOrchestrator();
@@ -1,4 +1,4 @@
1
- import type { Message } from "../../types.js";
1
+ import type { Message, ToolSchema } from "../../types.js";
2
2
  export type KernelAgentRole = "explore" | "plan" | "implement" | "verify" | "custom";
3
3
  export type AgentIsolation = "shared" | "read_only" | "worktree" | "remote";
4
4
  export type ContextInheritance = "none" | "system_only" | "full";
@@ -52,6 +52,9 @@ export interface LoopResult {
52
52
  export interface SubAgentResult {
53
53
  agentId: string;
54
54
  result: LoopResult;
55
+ /** R3-1: nodes this node's agent asked to append to the parent workflow DAG. Surfaced by the
56
+ * orchestrator; `runWorkflow` sends them to the parent kernel before completion. SDK-internal. */
57
+ submittedNodes?: WorkflowNodeSpec[];
55
58
  }
56
59
  export interface MilestoneCheckResult {
57
60
  phaseId: string;
@@ -82,12 +85,20 @@ export type WorkflowTaskSpec = {
82
85
  lane?: string;
83
86
  } | string;
84
87
  /** One node in a declarative workflow DAG (camelCase host shape). */
88
+ export type NodeTrust = "trusted" | "quarantined";
85
89
  export interface WorkflowNodeSpec {
86
90
  task: WorkflowTaskSpec;
87
91
  role: KernelAgentRole;
88
92
  isolation?: AgentIsolation;
89
93
  contextInheritance?: ContextInheritance;
90
94
  modelHint?: string;
95
+ /** W3: `quarantined` nodes read untrusted content and must run without privileges (read-only). */
96
+ trust?: NodeTrust;
97
+ /** G3: JSON Schema the node's output must conform to (validated + retried SDK-side). */
98
+ outputSchema?: Record<string, unknown>;
99
+ /** G2: make this a deterministic reduce node — runs no LLM; the runner routes it to the registered
100
+ * reducer of this name over its `dependsOn` nodes' outputs. */
101
+ reducer?: string;
91
102
  /** Indices of nodes this node depends on. */
92
103
  dependsOn?: number[];
93
104
  }
@@ -103,9 +114,36 @@ export interface WorkflowSpawnInfo {
103
114
  isolation: string;
104
115
  context_inheritance: string;
105
116
  model_hint?: string;
117
+ /** G3: JSON Schema the node's output must conform to (carried verbatim from the spec). */
118
+ output_schema?: Record<string, unknown>;
119
+ /** G2: for a reduce node, the registered reducer name to run (no LLM). */
120
+ reducer?: string;
121
+ /** G2: the dependency agent ids whose outputs a reduce node consumes. */
122
+ input_agent_ids?: string[];
106
123
  }
124
+ /** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
125
+ * `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
126
+ export interface WorkflowBudget {
127
+ nodes_used: number;
128
+ nodes_max?: number;
129
+ nodes_remaining?: number;
130
+ running_subagents: number;
131
+ max_concurrent_subagents?: number;
132
+ concurrency_remaining?: number;
133
+ }
134
+ /** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
135
+ export declare function workflowBudgetNote(budget: WorkflowBudget | undefined): string;
107
136
  /** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
137
+ /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
138
+ * `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
139
+ export declare function workflowNodeSpecToKernel(n: WorkflowNodeSpec): Record<string, unknown>;
108
140
  export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string, unknown>;
141
+ /** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
142
+ * `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
143
+ * nodes coerced to quarantined). Omitted ⇒ no coercion. */
144
+ export declare function submitWorkflowNodesToKernel(nodes: WorkflowNodeSpec[], submitterAgentId?: string): Record<string, unknown>;
145
+ /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
146
+ export declare const submitWorkflowNodesTool: ToolSchema;
109
147
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
110
148
  export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
111
149
  /** Build the host manifest for a kernel-generated workflow node. */
@@ -94,27 +94,87 @@ export function milestoneCheckPass(phaseId) {
94
94
  export function milestoneCheckFail(phaseId, reason) {
95
95
  return { phaseId, passed: false, reason };
96
96
  }
97
+ /** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
98
+ export function workflowBudgetNote(budget) {
99
+ if (!budget)
100
+ return "";
101
+ const parts = [];
102
+ if (budget.nodes_remaining != null && budget.nodes_max != null) {
103
+ parts.push(`nodes ${budget.nodes_used}/${budget.nodes_max} used, ${budget.nodes_remaining} remaining`);
104
+ }
105
+ if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
106
+ parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
107
+ }
108
+ if (parts.length === 0)
109
+ return "";
110
+ return (`[workflow budget] ${parts.join(" · ")}. ` +
111
+ "If you submit more workflow nodes, keep the batch within the remaining node budget.");
112
+ }
97
113
  /** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
114
+ /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
115
+ * `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
116
+ export function workflowNodeSpecToKernel(n) {
117
+ const task = typeof n.task === "string" ? { goal: n.task } : n.task;
118
+ 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
+ },
125
+ role: n.role,
126
+ isolation: n.isolation ?? "shared",
127
+ context_inheritance: n.contextInheritance ?? "none",
128
+ ...(n.modelHint ? { model_hint: n.modelHint } : {}),
129
+ ...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
130
+ ...(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 } } : {}),
133
+ ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
134
+ };
135
+ }
98
136
  export function workflowSpecToKernel(spec) {
137
+ return { nodes: spec.nodes.map(workflowNodeSpecToKernel) };
138
+ }
139
+ /** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
140
+ * `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
141
+ * nodes coerced to quarantined). Omitted ⇒ no coercion. */
142
+ export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
99
143
  return {
100
- nodes: spec.nodes.map(n => {
101
- const task = typeof n.task === "string" ? { goal: n.task } : n.task;
102
- return {
103
- task: {
104
- goal: task.goal,
105
- // `criteria` is required by the kernel's RuntimeTask serde (no default).
106
- criteria: task.criteria ?? [],
107
- ...(task.lane ? { lane: task.lane } : {}),
108
- },
109
- role: n.role,
110
- isolation: n.isolation ?? "shared",
111
- context_inheritance: n.contextInheritance ?? "none",
112
- ...(n.modelHint ? { model_hint: n.modelHint } : {}),
113
- ...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
114
- };
115
- }),
144
+ kind: "submit_workflow_nodes",
145
+ nodes: nodes.map(workflowNodeSpecToKernel),
146
+ ...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
116
147
  };
117
148
  }
149
+ /** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
150
+ export const submitWorkflowNodesTool = {
151
+ name: "submit_workflow_nodes",
152
+ 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.",
154
+ parameters: JSON.stringify({
155
+ type: "object",
156
+ 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
+ },
173
+ },
174
+ },
175
+ required: ["nodes"],
176
+ }),
177
+ };
118
178
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
119
179
  export function workflowNodeToSpec(node, parentSessionId) {
120
180
  return {
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { WorkflowNodeSpec } from "./runtime/types/agent.js";
1
2
  export interface Message {
2
3
  role: "system" | "user" | "assistant" | "tool";
3
4
  content: string;
@@ -63,6 +64,11 @@ export interface ToolResultEvent extends StreamEvent {
63
64
  isFatal?: boolean;
64
65
  errorKind?: ToolErrorKind;
65
66
  }
67
+ /** R3-1: a workflow node's agent called `submit_workflow_nodes`; the runner surfaces the requested nodes (the workflow lives in the parent kernel) and `runWorkflow` sends them to the parent kernel. */
68
+ export interface WorkflowNodesSubmittedEvent extends StreamEvent {
69
+ type: "workflow_nodes_submitted";
70
+ nodes: WorkflowNodeSpec[];
71
+ }
66
72
  export interface DoneEvent extends StreamEvent {
67
73
  type: "done";
68
74
  iterations: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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.9"
18
+ "@deepstrike/wasm-kernel": "0.2.11"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/jest": "^30.0.0",