@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.
@@ -5,27 +5,33 @@ import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-rep
5
5
  import { sanitizeReplayText } from "./replay-sanitize.js";
6
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, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
8
+ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
9
9
  import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
10
10
  import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
11
11
  import { resolveReducer } from "./reducers.js";
12
+ import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
12
13
  import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
13
14
  import { assertNativeProfile } from "./os-profile.js";
14
15
  import { LargeResultSpool } from "./large-result-spool.js";
15
16
  export class RuntimeRunner {
16
17
  opts;
17
18
  interrupted = false;
19
+ /** #2-B-ii: aborts the in-flight provider stream on interrupt/preempt. Recreated per `execute`. */
20
+ abortController = null;
18
21
  pendingObservations = [];
19
22
  activeKernel = null;
20
23
  currentSessionId = null;
21
24
  nextArchiveStart = 0;
22
25
  localPageOutCache = [];
26
+ /** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
27
+ * at the next safe point (after the tool turn resolves, kernel back in Reason). */
28
+ pendingAuthoredWorkflows = [];
23
29
  pendingSpoolOutputs = new Map();
24
30
  constructor(opts) {
25
31
  this.opts = opts;
26
32
  }
27
33
  get hostOptions() { return this.opts; }
28
- interrupt() { this.interrupted = true; }
34
+ interrupt() { this.interrupted = true; this.abortController?.abort(); }
29
35
  async *run(req) {
30
36
  const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
31
37
  const midRun = isMidRun(prior);
@@ -245,6 +251,7 @@ export class RuntimeRunner {
245
251
  }
246
252
  async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
247
253
  this.interrupted = false;
254
+ this.abortController = new AbortController();
248
255
  this.pendingObservations = [];
249
256
  this.pendingSpoolOutputs.clear();
250
257
  this.currentSessionId = sessionId;
@@ -258,6 +265,8 @@ export class RuntimeRunner {
258
265
  const runtime = new kernel.KernelRuntime({
259
266
  maxTokens: this.opts.maxTokens,
260
267
  maxTurns: effectiveMaxTurns,
268
+ // M4/G5: per-node token cap → child run's cumulative token budget (wasm LoopPolicy.maxTotalTokens is f64).
269
+ ...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
261
270
  timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
262
271
  });
263
272
  this.activeKernel = runtime;
@@ -407,24 +416,7 @@ export class RuntimeRunner {
407
416
  if (this.opts.signalSource) {
408
417
  const sig = await this.opts.signalSource.nextSignal();
409
418
  if (sig) {
410
- const id = crypto.randomUUID();
411
- const source = sig.source ?? "custom";
412
- const signalType = sig.signalType ?? "event";
413
- const urgency = sig.urgency ?? "normal";
414
- const summary = String(sig.payload?.goal ?? "signal");
415
- const sigAction = kernelMaybeAction(runtime, this.pendingObservations, {
416
- kind: "signal",
417
- signal: {
418
- id,
419
- source,
420
- signal_type: signalType,
421
- urgency,
422
- summary,
423
- payload: sig.payload ?? {},
424
- ...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
425
- timestamp_ms: Date.now(),
426
- },
427
- });
419
+ const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
428
420
  if (sigAction)
429
421
  action = sigAction;
430
422
  }
@@ -432,6 +424,12 @@ export class RuntimeRunner {
432
424
  if (runtime.isTerminal())
433
425
  break;
434
426
  if (action.kind === "call_provider") {
427
+ // M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
428
+ // placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
429
+ // authored spec is never stranded. Drains the queue; fires once per authored batch.
430
+ if (this.pendingAuthoredWorkflows.length > 0) {
431
+ action = await this.driveAuthoredWorkflows(runtime, action);
432
+ }
435
433
  const finalToolCalls = [];
436
434
  let finalText = "";
437
435
  const context = action.context;
@@ -440,8 +438,12 @@ export class RuntimeRunner {
440
438
  let turnInputTokens = 0;
441
439
  let turnOutputTokens = 0;
442
440
  let shouldRetry = false;
441
+ const abortSignal = this.abortController?.signal;
443
442
  try {
444
- for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState)) {
443
+ for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
444
+ // #2-B-ii: a preempting interrupt fires abortController — stop consuming the live stream.
445
+ if (abortSignal?.aborted)
446
+ break;
445
447
  if (evt.type === "usage") {
446
448
  const usageEvt = evt;
447
449
  turnTokens = usageEvt.totalTokens;
@@ -459,6 +461,10 @@ export class RuntimeRunner {
459
461
  }
460
462
  }
461
463
  catch (err) {
464
+ // #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat as an interrupt.
465
+ if (abortSignal?.aborted) {
466
+ this.interrupted = true;
467
+ }
462
468
  const errMsg = String(err).toLowerCase();
463
469
  if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
464
470
  !hasAttemptedReactiveCompact) {
@@ -474,6 +480,11 @@ export class RuntimeRunner {
474
480
  break;
475
481
  }
476
482
  }
483
+ // #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
484
+ if (abortSignal?.aborted) {
485
+ action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
486
+ break;
487
+ }
477
488
  if (shouldRetry) {
478
489
  action = {
479
490
  kind: "call_provider",
@@ -532,10 +543,26 @@ export class RuntimeRunner {
532
543
  // R3-1: intercept `submit_workflow_nodes` — it can't apply to this runner's kernel (when this
533
544
  // runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
534
545
  // 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");
546
+ // M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
547
+ const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
548
+ const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
537
549
  for (const call of submitCalls) {
538
- const nodes = parseSubmitWorkflowNodesArgs(call.arguments);
550
+ // M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
551
+ // spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
552
+ // every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
553
+ if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
554
+ const spec = parseStartWorkflowSpec(call.arguments);
555
+ if (spec) {
556
+ this.pendingAuthoredWorkflows.push(spec);
557
+ const out = "workflow authored; executing now";
558
+ toolResults.push({ callId: call.id, output: out, isError: false });
559
+ yield { type: "tool_result", callId: call.id, content: out, isError: false };
560
+ continue;
561
+ }
562
+ }
563
+ const nodes = call.name === "start_workflow"
564
+ ? parseStartWorkflowArgs(call.arguments)
565
+ : parseSubmitWorkflowNodesArgs(call.arguments);
539
566
  yield { type: "workflow_nodes_submitted", nodes };
540
567
  toolResults.push({ callId: call.id, output: "submitted", isError: false });
541
568
  yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
@@ -725,7 +752,7 @@ export class RuntimeRunner {
725
752
  * fed back on mismatch. If it still does not conform, the node is failed with the validation
726
753
  * reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
727
754
  */
728
- async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
755
+ async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs, abortSignal) {
729
756
  // G2: a reduce node runs no LLM — execute the registered pure function over its dependency
730
757
  // outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
731
758
  if (node.reducer) {
@@ -742,7 +769,40 @@ export class RuntimeRunner {
742
769
  spec: { ...baseSpec, goal: withBudget(goal) },
743
770
  manifest,
744
771
  sessionLog: this.opts.sessionLog,
772
+ // M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel.
773
+ isWorkflowNode: true,
774
+ // #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
775
+ ...(abortSignal ? { abortSignal } : {}),
745
776
  });
777
+ const textOf = (r) => {
778
+ const c = r.result.finalMessage?.content;
779
+ return typeof c === "string" ? c : c != null ? JSON.stringify(c) : "";
780
+ };
781
+ const withSignal = (r, patch) => ({ ...r, result: { ...r.result, ...patch } });
782
+ // A#2 tournament judge: compare two entrants' produced outputs rather than running the node's own
783
+ // goal. Look up both candidates, judge over the controller's criterion, and report the winner's id.
784
+ if (node.judge_match) {
785
+ const out = outputs ?? new Map();
786
+ const left = out.get(node.judge_match.left) ?? "";
787
+ const right = out.get(node.judge_match.right) ?? "";
788
+ const result = await orchestrator.run(mkCtx(judgeGoal(baseSpec.goal, left, right)));
789
+ const winner = extractJudgeWinner(textOf(result));
790
+ const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
791
+ return withSignal(result, { tournamentWinner: winnerId });
792
+ }
793
+ // A#2 v2 loop iteration: run the increment, then extract a stop signal. No signal ⇒ run to cap.
794
+ if (node.loop_max_iters != null) {
795
+ const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters)}`));
796
+ const cont = extractLoopContinue(textOf(result));
797
+ return cont === undefined ? result : withSignal(result, { loopContinue: cont });
798
+ }
799
+ // A#2 classify: run the classifier, then extract the chosen branch label (kernel prunes the rest).
800
+ if (node.classify_labels && node.classify_labels.length) {
801
+ const labels = node.classify_labels;
802
+ const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${classifyInstruction(labels)}`));
803
+ const branch = extractClassifyBranch(textOf(result), labels);
804
+ return branch === undefined ? result : withSignal(result, { classifyBranch: branch });
805
+ }
746
806
  const schema = node.output_schema;
747
807
  if (!schema)
748
808
  return orchestrator.run(mkCtx(baseSpec.goal));
@@ -805,7 +865,6 @@ export class RuntimeRunner {
805
865
  }
806
866
  const parentSessionId = this.currentSessionId;
807
867
  const runtime = this.activeKernel;
808
- const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
809
868
  const observations = kernelApply(runtime, this.pendingObservations, {
810
869
  kind: "load_workflow",
811
870
  spec: workflowSpecToKernel(spec),
@@ -815,22 +874,103 @@ export class RuntimeRunner {
815
874
  // R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
816
875
  ...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
817
876
  });
877
+ return this.driveWorkflow(observations, parentSessionId, runtime);
878
+ }
879
+ /**
880
+ * M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Routes the
881
+ * spec through the agent-reachable `Syscall::LoadWorkflow` (`submit_workflow`): with no workflow
882
+ * active the kernel bootstraps the DAG, else it flattens onto the running one (bootstrap-or-flatten —
883
+ * one kernel, one quota). The same shared driver runs the resulting batches.
884
+ */
885
+ async bootstrapWorkflow(spec, opts) {
886
+ if (!this.activeKernel || !this.currentSessionId) {
887
+ throw new Error("bootstrapWorkflow requires an active parent run");
888
+ }
889
+ const parentSessionId = this.currentSessionId;
890
+ const runtime = this.activeKernel;
891
+ const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
892
+ return this.driveWorkflow(observations, parentSessionId, runtime);
893
+ }
894
+ /**
895
+ * M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
896
+ * point (tool turn resolved → kernel in Reason). Each runs in THIS kernel (the kernel resumes the
897
+ * reason loop on `workflow_completed`), then the outcome is injected as a user message and a fresh
898
+ * `call_provider` is synthesized from the updated context (the workflow drive consumed its own
899
+ * kernel actions — same re-render pattern as the reactive-compact retry path).
900
+ */
901
+ async driveAuthoredWorkflows(runtime, action) {
902
+ const specs = this.pendingAuthoredWorkflows;
903
+ this.pendingAuthoredWorkflows = [];
904
+ for (const spec of specs) {
905
+ const outcome = await this.bootstrapWorkflow(spec);
906
+ kernelApply(runtime, this.pendingObservations, {
907
+ kind: "add_history_message",
908
+ message: messageToKernelMessage({ role: "user", content: authoredWorkflowOutcomeNote(outcome) }),
909
+ });
910
+ }
911
+ return { kind: "call_provider", context: runtime.render(), tools: action.tools };
912
+ }
913
+ /**
914
+ * #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
915
+ * routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
916
+ * `WorkflowRun` down), and we abort the matching children's in-flight LLM calls. Returns the
917
+ * torn-down outcome on preemption, else `null`. No-op without a signal source.
918
+ */
919
+ async monitorWorkflowPreemption(runtime, controllers, batchState) {
920
+ const source = this.opts.signalSource;
921
+ if (!source)
922
+ return null;
923
+ while (!batchState.settled) {
924
+ const sig = await source.nextSignal();
925
+ if (batchState.settled)
926
+ break;
927
+ if (!sig) {
928
+ await new Promise(resolve => setTimeout(resolve, 5));
929
+ continue;
930
+ }
931
+ const obs = kernelApply(runtime, this.pendingObservations, signalToKernelEvent(sig));
932
+ const preempted = obs.find(o => o.kind === "agent_preempted");
933
+ if (preempted) {
934
+ for (const id of preempted.agent_ids ?? [])
935
+ controllers.get(id)?.abort();
936
+ const wc = obs.find(o => o.kind === "workflow_completed");
937
+ return { completed: wc?.completed ?? [], failed: wc?.failed ?? [] };
938
+ }
939
+ }
940
+ return null;
941
+ }
942
+ /**
943
+ * Shared workflow driver for `runWorkflow` (host `load_workflow`) and `bootstrapWorkflow` (agent
944
+ * `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
945
+ * agent-submitted nodes first), and loop until the kernel reports the workflow complete.
946
+ */
947
+ async driveWorkflow(initial, parentSessionId, runtime) {
948
+ const observations = initial;
949
+ const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
818
950
  const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
819
951
  // G4: the batch observation carries the workflow's remaining budget; track the latest.
820
952
  const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
821
953
  const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
822
954
  let done = findDone(observations);
823
955
  if (done)
824
- return { completed: done.completed ?? [], failed: done.failed ?? [] };
956
+ return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: {} };
825
957
  let nodes = collectNodes(observations);
826
958
  let budget = collectBudget(observations);
827
959
  // G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
828
960
  const outputs = new Map();
829
961
  for (;;) {
830
962
  if (nodes.length === 0)
831
- return { completed: [], failed: [] };
963
+ return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
832
964
  const roundBudget = budget;
833
- const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs)));
965
+ // #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
966
+ const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
967
+ const batchState = { settled: false };
968
+ const monitor = this.monitorWorkflowPreemption(runtime, controllers, batchState);
969
+ const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs, controllers.get(node.agent_id)?.signal)));
970
+ batchState.settled = true;
971
+ const preempted = await monitor;
972
+ if (preempted)
973
+ return { ...preempted, outputs: Object.fromEntries(outputs) };
834
974
  // Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
835
975
  const nextNodes = [];
836
976
  done = undefined;
@@ -871,7 +1011,7 @@ export class RuntimeRunner {
871
1011
  }));
872
1012
  }
873
1013
  if (done && nextNodes.length === 0) {
874
- return { completed: done.completed ?? [], failed: done.failed ?? [] };
1014
+ return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: Object.fromEntries(outputs) };
875
1015
  }
876
1016
  nodes = nextNodes;
877
1017
  }
@@ -1090,3 +1230,61 @@ function parseSubmitWorkflowNodesArgs(argsStr) {
1090
1230
  }
1091
1231
  return Array.isArray(parsed.nodes) ? parsed.nodes : [];
1092
1232
  }
1233
+ /** M5 v1: parse `start_workflow` tool args (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into the
1234
+ * spec's node batch — flattened onto the running workflow via the same append path. */
1235
+ function parseStartWorkflowArgs(argsStr) {
1236
+ let parsed = {};
1237
+ try {
1238
+ parsed = JSON.parse(argsStr);
1239
+ }
1240
+ catch {
1241
+ // Ignore parse error → no nodes.
1242
+ }
1243
+ const spec = parsed.spec;
1244
+ return Array.isArray(spec?.nodes) ? spec.nodes : [];
1245
+ }
1246
+ /** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call for the auto-pivot
1247
+ * drive. Returns `undefined` on a malformed / empty payload (caller falls back to the flatten path). */
1248
+ function parseStartWorkflowSpec(argsStr) {
1249
+ try {
1250
+ const parsed = JSON.parse(argsStr);
1251
+ if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
1252
+ return { nodes: parsed.spec.nodes };
1253
+ }
1254
+ }
1255
+ catch {
1256
+ // Ignore parse error → undefined (fall back to flatten).
1257
+ }
1258
+ return undefined;
1259
+ }
1260
+ /** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
1261
+ * agent's context, so its next turn continues with the sub-workflow's results in view. */
1262
+ function authoredWorkflowOutcomeNote(outcome) {
1263
+ const lines = [
1264
+ `[authored workflow result] ${outcome.completed.length} node(s) completed` +
1265
+ (outcome.failed.length ? `, ${outcome.failed.length} failed` : "") + ".",
1266
+ ];
1267
+ for (const id of outcome.completed) {
1268
+ const out = outcome.outputs[id];
1269
+ if (out)
1270
+ lines.push(`- ${id}: ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
1271
+ }
1272
+ return lines.join("\n");
1273
+ }
1274
+ /** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
1275
+ * loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
1276
+ function signalToKernelEvent(sig) {
1277
+ return {
1278
+ kind: "signal",
1279
+ signal: {
1280
+ id: crypto.randomUUID(),
1281
+ source: sig.source ?? "custom",
1282
+ signal_type: sig.signalType ?? "event",
1283
+ urgency: sig.urgency ?? "normal",
1284
+ summary: String(sig.payload?.goal ?? "signal"),
1285
+ payload: sig.payload ?? {},
1286
+ ...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
1287
+ timestamp_ms: Date.now(),
1288
+ },
1289
+ };
1290
+ }
@@ -7,7 +7,16 @@ export interface SubAgentRunContext {
7
7
  spec: AgentRunSpec;
8
8
  manifest: AgentProcessChangedObservation;
9
9
  sessionLog: SessionLog;
10
+ /** M5 v2.1: set when this child is a workflow node — propagated so a nested `start_workflow`
11
+ * FLATTENS to the parent kernel rather than auto-pivoting into its own bootstrap. */
12
+ isWorkflowNode?: boolean;
13
+ /** #2-B-ii: parent-controlled abort — when the kernel preempts this node (`AgentPreempted`), the
14
+ * orchestrator interrupts the child runner, cancelling its in-flight LLM call. */
15
+ abortSignal?: AbortSignal;
10
16
  }
17
+ /** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
18
+ * Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
19
+ export declare function resolveProvider(opts: RuntimeOptions, modelHint?: string): RuntimeOptions["provider"];
11
20
  /** Host-side driver for kernel-isolated sub-agent runs. */
12
21
  export declare class SubAgentOrchestrator {
13
22
  run(ctx: SubAgentRunContext): Promise<SubAgentResult>;
@@ -1,6 +1,16 @@
1
1
  import { agentRunSpecToKernel, findSpawnProcessObservation, spawnObservationToManifest } from "./types/agent.js";
2
2
  import { FilteredExecutionPlane } from "./filtered-plane.js";
3
3
  import { kernelApply } from "./kernel-step.js";
4
+ /** #2-B-ii: bridge a parent AbortSignal to a child runner's `interrupt()` (fires now if already aborted). */
5
+ function linkAbort(signal, runner) {
6
+ if (!signal)
7
+ return;
8
+ if (signal.aborted) {
9
+ runner.interrupt();
10
+ return;
11
+ }
12
+ signal.addEventListener("abort", () => runner.interrupt(), { once: true });
13
+ }
4
14
  function terminationFromStatus(status) {
5
15
  const normalized = status.toLowerCase();
6
16
  if (normalized === "completed" ||
@@ -14,6 +24,16 @@ function terminationFromStatus(status) {
14
24
  }
15
25
  return status;
16
26
  }
27
+ /** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
28
+ * Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
29
+ export function resolveProvider(opts, modelHint) {
30
+ if (modelHint && opts.providerFor) {
31
+ const routed = opts.providerFor(modelHint);
32
+ if (routed)
33
+ return routed;
34
+ }
35
+ return opts.provider;
36
+ }
17
37
  /** Derive which meta-tools a child runner should expose based on permitted IDs and available sources. */
18
38
  function deriveMetaTools(permitted, opts) {
19
39
  const metaTools = new Set();
@@ -48,6 +68,10 @@ export class SubAgentOrchestrator {
48
68
  const { RuntimeRunner } = await import("./runner.js");
49
69
  const childRunner = new RuntimeRunner({
50
70
  ...ctx.parentOpts,
71
+ // M1/G3: route to the node's hinted model (falls back to the parent provider).
72
+ provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
73
+ // M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
74
+ maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
51
75
  executionPlane: filteredPlane,
52
76
  agentId: ctx.spec.identity.agentId,
53
77
  systemPrompt,
@@ -56,7 +80,11 @@ export class SubAgentOrchestrator {
56
80
  dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
57
81
  knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
58
82
  enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
83
+ // M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
84
+ isWorkflowNode: ctx.isWorkflowNode,
59
85
  });
86
+ // #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
87
+ linkAbort(ctx.abortSignal, childRunner);
60
88
  let done;
61
89
  let finalText = "";
62
90
  // R3-1: collect any nodes this node's agent submitted via the `submit_workflow_nodes` tool.
@@ -23,6 +23,11 @@ export interface AgentRunSpec {
23
23
  capabilityFilter?: AgentCapabilityFilter;
24
24
  milestones?: MilestoneContract;
25
25
  metadata?: Record<string, unknown>;
26
+ /** M1/G3: per-agent model preference; the host resolves it via `RuntimeOptions.providerFor`.
27
+ * Host-side routing only — not sent to the kernel. */
28
+ modelHint?: string;
29
+ /** M4/G5: cumulative token cap for this sub-agent's run (sets the child kernel's `maxTotalTokens`). */
30
+ tokenBudget?: number;
26
31
  }
27
32
  /** Kernel process-table observation (Phase 3 canonical spawn signal). */
28
33
  export interface AgentProcessChangedObservation {
@@ -48,6 +53,14 @@ export interface LoopResult {
48
53
  finalMessage?: Message;
49
54
  turnsUsed: number;
50
55
  totalTokensUsed: number;
56
+ /** A#2 v2 loop stop signal: a loop iteration sets `false` to end the loop before `max_iters`.
57
+ * `undefined` (every non-loop result) ⇒ no opinion → run to the cap. Sent only when set. */
58
+ loopContinue?: boolean;
59
+ /** A#2 classify routing: a classifier node reports the chosen branch label here; the kernel runs
60
+ * that branch and prunes the rest. Sent only when set. */
61
+ classifyBranch?: string;
62
+ /** A#2 tournament verdict: a judge reports the winning entrant's agent id here. Sent only when set. */
63
+ tournamentWinner?: string;
51
64
  }
52
65
  export interface SubAgentResult {
53
66
  agentId: string;
@@ -99,6 +112,26 @@ export interface WorkflowNodeSpec {
99
112
  /** G2: make this a deterministic reduce node — runs no LLM; the runner routes it to the registered
100
113
  * reducer of this name over its `dependsOn` nodes' outputs. */
101
114
  reducer?: string;
115
+ /** A#2 v2: make this a *loop* node — re-run its agent up to `maxIters` times. An iteration may end
116
+ * the loop early by reporting `loopContinue: false` (the runner solicits this from the agent). */
117
+ loop?: {
118
+ maxIters: number;
119
+ };
120
+ /** A#2: make this a *classify* node — its agent picks exactly one branch `label`; that branch's
121
+ * nodes run and the others are pruned. Each branch node must list this node's index in `dependsOn`. */
122
+ classify?: {
123
+ branches: Array<{
124
+ label: string;
125
+ nodes: number[];
126
+ }>;
127
+ };
128
+ /** A#2: make this a *tournament controller* — generate each `entrants` candidate in parallel, then
129
+ * pairwise-judge them to one winner (this node's `task.goal` is the judging criterion). ≥2 entrants. */
130
+ tournament?: {
131
+ entrants: WorkflowTaskSpec[];
132
+ };
133
+ /** M4/G5: cap this node's child run at `tokenBudget` cumulative tokens (the per-node "use N tokens"). */
134
+ tokenBudget?: number;
102
135
  /** Indices of nodes this node depends on. */
103
136
  dependsOn?: number[];
104
137
  }
@@ -120,6 +153,20 @@ export interface WorkflowSpawnInfo {
120
153
  reducer?: string;
121
154
  /** G2: the dependency agent ids whose outputs a reduce node consumes. */
122
155
  input_agent_ids?: string[];
156
+ /** A#2: present only for a tournament *judge* spawn — the two entrant agent ids whose produced
157
+ * outputs this judge compares. The runner looks them up and reports the winner as `tournamentWinner`. */
158
+ judge_match?: {
159
+ left: string;
160
+ right: string;
161
+ };
162
+ /** A#2 v2: present only for a *loop* iteration spawn — the loop's `max_iters`. Marks the spawn as a
163
+ * loop iteration so the runner solicits + reports a `loopContinue` stop signal. */
164
+ loop_max_iters?: number;
165
+ /** A#2: present only for a *classify* spawn — the branch labels the classifier must choose among.
166
+ * Non-empty marks the spawn as a classifier so the runner instructs the agent + reports `classifyBranch`. */
167
+ classify_labels?: string[];
168
+ /** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
169
+ token_budget?: number;
123
170
  }
124
171
  /** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
125
172
  * `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
@@ -130,10 +177,14 @@ export interface WorkflowBudget {
130
177
  running_subagents: number;
131
178
  max_concurrent_subagents?: number;
132
179
  concurrency_remaining?: number;
180
+ /** M4/G5 token headroom: tokens used / run-level cap / tokens remaining, so a coordinator can scale
181
+ * a submission to "use N tokens". */
182
+ tokens_used?: number;
183
+ tokens_max?: number;
184
+ tokens_remaining?: number;
133
185
  }
134
186
  /** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
135
187
  export declare function workflowBudgetNote(budget: WorkflowBudget | undefined): string;
136
- /** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
137
188
  /** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
138
189
  * `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
139
190
  export declare function workflowNodeSpecToKernel(n: WorkflowNodeSpec): Record<string, unknown>;
@@ -142,8 +193,16 @@ export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string,
142
193
  * `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
143
194
  * nodes coerced to quarantined). Omitted ⇒ no coercion. */
144
195
  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. */
196
+ /** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
197
+ * `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
198
+ * `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
199
+ * on the flatten case. */
200
+ export declare function submitWorkflowToKernel(spec: WorkflowSpec, parentSessionId: string, submitterAgentId?: string): Record<string, unknown>;
146
201
  export declare const submitWorkflowNodesTool: ToolSchema;
202
+ /** M5 v1 (flatten): the tool an agent calls to author a sub-workflow — a cohesive DAG of nodes
203
+ * composed onto the running workflow. Lowers to the same append path as `submit_workflow_nodes`
204
+ * (a `WorkflowSpec` is a node batch). v2 adds top-level bootstrap (the `LoadWorkflow` syscall). */
205
+ export declare const startWorkflowTool: ToolSchema;
147
206
  /** Build a sub-agent run spec for a kernel-generated workflow node. */
148
207
  export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
149
208
  /** Build the host manifest for a kernel-generated workflow node. */