@agwab/pi-workflow 0.2.0 → 0.3.0

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.
Files changed (79) hide show
  1. package/README.md +2 -0
  2. package/dist/compiler.d.ts +4 -6
  3. package/dist/compiler.js +70 -39
  4. package/dist/dynamic-decision.d.ts +0 -1
  5. package/dist/dynamic-decision.js +0 -7
  6. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  7. package/dist/dynamic-generated-task-runtime.js +21 -8
  8. package/dist/dynamic-profiles.d.ts +0 -1
  9. package/dist/dynamic-profiles.js +0 -3
  10. package/dist/engine-run-graph.d.ts +1 -0
  11. package/dist/engine-run-graph.js +142 -2
  12. package/dist/engine.d.ts +10 -6
  13. package/dist/engine.js +146 -77
  14. package/dist/extension.d.ts +2 -1
  15. package/dist/extension.js +38 -15
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -1
  18. package/dist/store.d.ts +3 -1
  19. package/dist/store.js +189 -49
  20. package/dist/subagent-backend.d.ts +4 -0
  21. package/dist/subagent-backend.js +281 -31
  22. package/dist/types.d.ts +9 -1
  23. package/dist/workflow-runtime.d.ts +2 -0
  24. package/dist/workflow-runtime.js +40 -1
  25. package/dist/workflow-view.js +3 -1
  26. package/dist/workflow-web-source-extension.js +167 -48
  27. package/dist/workflow-web-source.d.ts +2 -1
  28. package/dist/workflow-web-source.js +84 -19
  29. package/docs/usage.md +11 -0
  30. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  31. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  32. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  33. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  34. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  35. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  36. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  37. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  38. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  39. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  40. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  41. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  42. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  43. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  44. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  45. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  46. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  47. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  48. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  49. package/package.json +2 -2
  50. package/src/compiler.ts +127 -66
  51. package/src/dynamic-decision.ts +0 -11
  52. package/src/dynamic-generated-task-runtime.ts +47 -12
  53. package/src/dynamic-profiles.ts +0 -4
  54. package/src/engine-run-graph.ts +185 -2
  55. package/src/engine.ts +192 -107
  56. package/src/extension.ts +50 -17
  57. package/src/index.ts +3 -1
  58. package/src/store.ts +253 -55
  59. package/src/subagent-backend.ts +369 -32
  60. package/src/types.ts +13 -1
  61. package/src/workflow-runtime.ts +53 -2
  62. package/src/workflow-view.ts +2 -1
  63. package/src/workflow-web-source-extension.ts +621 -228
  64. package/src/workflow-web-source.ts +118 -28
  65. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  66. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  67. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  68. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  69. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  70. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  71. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  72. package/workflows/impact-review/spec.json +3 -3
  73. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  74. package/dist/dynamic-loader.d.ts +0 -25
  75. package/dist/dynamic-loader.js +0 -13
  76. package/src/dynamic-loader.ts +0 -49
  77. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  78. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  79. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -79,6 +79,13 @@ export interface PiJsonParseResult {
79
79
  metadata: Partial<ResultMetadata>;
80
80
  }
81
81
 
82
+ export interface ContextLengthResolution {
83
+ rawContextLengthExceeded: boolean;
84
+ contextLengthExceeded: boolean;
85
+ contextOverflowRecovered: boolean;
86
+ recoveredStreamErrors: string[];
87
+ }
88
+
82
89
  const CONTEXT_LENGTH_ERROR_PATTERN =
83
90
  /\bcontext[_ -]?length[_ -]?exceeded\b|\bcontext[_ -]?window[_ -]?(?:exceeded|overflow|exhausted)\b|\b(?:maximum|max)[_ -]?context[_ -]?length\b|\btoo many tokens\b|\b(?:prompt|input|request)[^\n]{0,80}\btoo large\b|\bcontext_length_exceeded\b/i;
84
91
 
@@ -94,6 +101,32 @@ export function detectContextLengthExceeded(signals: {
94
101
  return CONTEXT_LENGTH_ERROR_PATTERN.test(text);
95
102
  }
96
103
 
104
+ export function resolveContextLengthState(
105
+ parsed: PiJsonParseResult,
106
+ rawContextLengthExceeded: boolean,
107
+ ): ContextLengthResolution {
108
+ const contextOverflowRecovered =
109
+ rawContextLengthExceeded && finalAssistantSucceeded(parsed);
110
+ return {
111
+ rawContextLengthExceeded,
112
+ contextLengthExceeded:
113
+ rawContextLengthExceeded && !contextOverflowRecovered,
114
+ contextOverflowRecovered,
115
+ recoveredStreamErrors: contextOverflowRecovered
116
+ ? parsed.errors.filter((error) =>
117
+ detectContextLengthExceeded({ errors: [error] }),
118
+ )
119
+ : [],
120
+ };
121
+ }
122
+
123
+ function finalAssistantSucceeded(parsed: PiJsonParseResult): boolean {
124
+ return (
125
+ parsed.finalAssistantText.length > 0 &&
126
+ parsed.metadata.stopReason !== "error"
127
+ );
128
+ }
129
+
97
130
  function normalizeTimeoutMs(timeoutMs: number | undefined): number | undefined {
98
131
  if (timeoutMs === undefined) return undefined;
99
132
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
@@ -390,18 +423,29 @@ export function resolvePiJsonOutcome(
390
423
 
391
424
  export function resultMetadataFromParse(
392
425
  parsed: PiJsonParseResult,
393
- contextLengthExceeded: boolean,
426
+ contextLength: ContextLengthResolution,
394
427
  outcome: ProcessOutcome,
395
428
  ): Partial<ResultMetadata> {
396
429
  return {
397
430
  ...parsed.metadata,
398
- contextLengthExceeded,
431
+ contextLengthExceeded: contextLength.contextLengthExceeded,
432
+ ...(contextLength.contextOverflowRecovered
433
+ ? { contextOverflowRecovered: true }
434
+ : {}),
399
435
  ...(parsed.errors.length === 0
400
436
  ? {}
401
437
  : { streamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }),
402
438
  ...(outcome.status === "completed" && parsed.errors.length > 0
403
439
  ? { nonFatalStreamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }
404
440
  : {}),
441
+ ...(contextLength.recoveredStreamErrors.length === 0
442
+ ? {}
443
+ : {
444
+ recoveredStreamErrors: contextLength.recoveredStreamErrors.slice(
445
+ 0,
446
+ MAX_METADATA_ERRORS,
447
+ ),
448
+ }),
405
449
  ...(parsed.parseErrors.length === 0
406
450
  ? {}
407
451
  : { parseErrors: parsed.parseErrors.slice(0, MAX_METADATA_ERRORS) }),
@@ -774,14 +818,18 @@ export async function runHeadlessModel(
774
818
  stderrText,
775
819
  stderrContextLengthExceeded,
776
820
  } = processResult;
777
- const contextLengthExceeded =
821
+ const rawContextLengthExceeded =
778
822
  stderrContextLengthExceeded ||
779
823
  detectContextLengthExceeded({ stderrText, errors: parsed.errors });
824
+ const contextLength = resolveContextLengthState(
825
+ parsed,
826
+ rawContextLengthExceeded,
827
+ );
780
828
 
781
829
  const outcome = resolvePiJsonOutcome(
782
830
  processOutcome,
783
831
  parsed,
784
- contextLengthExceeded,
832
+ contextLength.contextLengthExceeded,
785
833
  );
786
834
 
787
835
  const completedAt = new Date();
@@ -811,7 +859,7 @@ export async function runHeadlessModel(
811
859
  artifacts,
812
860
  correlationId: options.correlationId,
813
861
  metadata: {
814
- ...resultMetadataFromParse(parsed, contextLengthExceeded, outcome),
862
+ ...resultMetadataFromParse(parsed, contextLength, outcome),
815
863
  ...sessionMetadata,
816
864
  ...(options.parentSessionId === undefined
817
865
  ? {}
@@ -20,6 +20,7 @@ import {
20
20
  detectContextLengthExceeded,
21
21
  parsePiJsonFile,
22
22
  parsePiJsonLines,
23
+ resolveContextLengthState,
23
24
  resolvePiJsonOutcome,
24
25
  resultMetadataFromParse,
25
26
  resultSessionMetadata,
@@ -157,9 +158,7 @@ function workerScript(
157
158
  return `import { spawn } from "node:child_process";\nimport { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs";\nconst argv = ${JSON.stringify(argv)};\nconst cwd = ${JSON.stringify(cwd)};\nconst eventPath = ${JSON.stringify(eventPath)};\nconst stderrPath = ${JSON.stringify(stderrPath)};\nconst metaPath = ${JSON.stringify(metaPath)};\nconst messageUpdatePattern = /"type"\\s*:\\s*"message_update"/;\nconst maxStdoutLogLineChars = 64 * 1024 * 1024;\ncloseSync(openSync(eventPath, "w"));\ncloseSync(openSync(stderrPath, "w"));\nlet settled = false;\nlet stdoutBuffer = "";\nlet discardingOversizedLine = false;\nlet omittedMessageUpdates = 0;\nlet omittedMessageUpdateBytes = 0;\nlet omittedOversizedLines = 0;\nlet omittedOversizedBytes = 0;\nfunction writeStdoutLine(line) {\n if (messageUpdatePattern.test(line)) {\n omittedMessageUpdates += 1;\n omittedMessageUpdateBytes += Buffer.byteLength(line, "utf8");\n return;\n }\n appendFileSync(eventPath, line);\n process.stdout.write(line);\n}\nfunction handleStdoutChunk(chunk) {\n let text = chunk.toString("utf8");\n while (text.length > 0) {\n if (discardingOversizedLine) {\n const newline = text.indexOf("\\n");\n omittedOversizedBytes += Buffer.byteLength(newline < 0 ? text : text.slice(0, newline + 1), "utf8");\n if (newline < 0) return;\n discardingOversizedLine = false;\n text = text.slice(newline + 1);\n continue;\n }\n const newline = text.indexOf("\\n");\n const segment = newline < 0 ? text : text.slice(0, newline + 1);\n stdoutBuffer += segment;\n text = newline < 0 ? "" : text.slice(newline + 1);\n if (stdoutBuffer.length > maxStdoutLogLineChars) {\n omittedOversizedLines += 1;\n omittedOversizedBytes += Buffer.byteLength(stdoutBuffer, "utf8");\n stdoutBuffer = "";\n discardingOversizedLine = newline < 0;\n continue;\n }\n if (newline >= 0) {\n writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n }\n }\n}\nfunction finishStdoutFilter() {\n if (!discardingOversizedLine && stdoutBuffer.length > 0) writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n if (omittedMessageUpdates > 0 || omittedOversizedLines > 0) {\n appendFileSync(eventPath, JSON.stringify({ type: "pi-subagent.stdout_filter", omitted: { messageUpdateEvents: omittedMessageUpdates, messageUpdateBytes: omittedMessageUpdateBytes, oversizedLines: omittedOversizedLines, oversizedBytes: omittedOversizedBytes }, reason: "cumulative message_update snapshots are omitted from durable stdout artifacts; final assistant text is stored in output.log" }) + "\\n");\n }\n}\nfunction writeMeta(meta) {\n if (settled) return;\n settled = true;\n finishStdoutFilter();\n writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\\n");\n}\nconst env = { ...process.env };\ndelete env.TMUX;\nconst child = spawn(argv[0], argv.slice(1), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], env });\nchild.stdout?.on("data", handleStdoutChunk);\nchild.stderr?.on("data", (chunk) => { appendFileSync(stderrPath, chunk); process.stderr.write(chunk); });\nchild.on("error", () => { writeMeta({ status: "failed", failureKind: "spawn", exitCode: null, signal: null }); });\nchild.on("close", (exitCode, signal) => {\n const failureKind = exitCode === 0 ? null : "exit";\n writeMeta({ status: failureKind === null ? "completed" : "failed", failureKind, exitCode, signal });\n});\n`;
158
159
  }
159
160
 
160
- async function runTmuxProcess(
161
- options: RunTmuxProcessOptions,
162
- ): Promise<{
161
+ async function runTmuxProcess(options: RunTmuxProcessOptions): Promise<{
163
162
  result: TmuxRunResult | null;
164
163
  store: Awaited<ReturnType<typeof createAttemptArtifactStore>>;
165
164
  cwd: string;
@@ -439,11 +438,19 @@ export async function runTmuxModel(
439
438
  parsePiJsonLines(""),
440
439
  );
441
440
  await unlink(result.eventPath).catch(() => undefined);
442
- const contextLengthExceeded = detectContextLengthExceeded({
441
+ const rawContextLengthExceeded = detectContextLengthExceeded({
443
442
  stderrText,
444
443
  errors: parsed.errors,
445
444
  });
446
- const meta = resolvePiJsonOutcome(result.meta, parsed, contextLengthExceeded);
445
+ const contextLength = resolveContextLengthState(
446
+ parsed,
447
+ rawContextLengthExceeded,
448
+ );
449
+ const meta = resolvePiJsonOutcome(
450
+ result.meta,
451
+ parsed,
452
+ contextLength.contextLengthExceeded,
453
+ );
447
454
 
448
455
  const outputRef = await store.writeTextArtifact(
449
456
  "output",
@@ -464,7 +471,7 @@ export async function runTmuxModel(
464
471
  tmux: result.tmux,
465
472
  correlationId: options.correlationId,
466
473
  metadata: {
467
- ...resultMetadataFromParse(parsed, contextLengthExceeded, meta),
474
+ ...resultMetadataFromParse(parsed, contextLength, meta),
468
475
  ...sessionMetadata,
469
476
  ...(options.parentSessionId === undefined
470
477
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agwab/pi-workflow",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Workflow orchestration for Pi subagents.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -76,7 +76,7 @@
76
76
  "node": ">=22.19.0"
77
77
  },
78
78
  "dependencies": {
79
- "@agwab/pi-subagent": "^0.3.6",
79
+ "@agwab/pi-subagent": "^0.4.0",
80
80
  "pi-web-access": "^0.10.7",
81
81
  "typebox": "^1.1.39"
82
82
  },
package/src/compiler.ts CHANGED
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
3
3
 
4
4
  import { loadAgentByName } from "./agents.js";
5
5
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
6
+ import { compileRole } from "./roles.js";
6
7
  import {
7
8
  classifyToolCapability,
8
9
  effectiveToolClassification,
@@ -33,7 +34,10 @@ import {
33
34
  } from "./types.js";
34
35
  import {
35
36
  resolveWorkflowRuntime,
37
+ selectWorkflowRuntime,
36
38
  type WorkflowModelInfo,
39
+ type WorkflowRuntimeDefaults,
40
+ type WorkflowRuntimeResolutionInput,
37
41
  } from "./workflow-runtime.js";
38
42
 
39
43
  const DELEGATION_TOOLS = new Set([
@@ -549,7 +553,8 @@ export async function compileWorkflow(
549
553
  spec: ArtifactGraphWorkflowSpec,
550
554
  options: CompileOptions & {
551
555
  task?: string;
552
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
556
+ runtimeOverrides?: WorkflowRuntimeDefaults;
557
+ runtimeDefaults?: WorkflowRuntimeDefaults;
553
558
  },
554
559
  ): Promise<any> {
555
560
  const compilePlan = buildArtifactGraphCompilePlan(spec, options);
@@ -615,11 +620,25 @@ async function collectForeachPathWarnings(
615
620
  return warnings;
616
621
  }
617
622
 
623
+ function runtimeSettings(value: unknown): WorkflowRuntimeDefaults | undefined {
624
+ if (!isPlainRecord(value)) return undefined;
625
+ const model =
626
+ typeof value.model === "string" && value.model.trim()
627
+ ? value.model.trim()
628
+ : undefined;
629
+ const thinking =
630
+ typeof value.thinking === "string" && value.thinking.trim()
631
+ ? (value.thinking.trim() as ThinkingLevel)
632
+ : undefined;
633
+ return model || thinking ? { model, thinking } : undefined;
634
+ }
635
+
618
636
  async function compileArtifactGraphPlan(
619
637
  spec: any,
620
638
  options: CompileOptions & {
621
639
  task?: string;
622
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
640
+ runtimeOverrides?: WorkflowRuntimeDefaults;
641
+ runtimeDefaults?: WorkflowRuntimeDefaults;
623
642
  },
624
643
  ): Promise<any> {
625
644
  const stages = spec.stages;
@@ -645,15 +664,19 @@ async function compileArtifactGraphPlan(
645
664
  return defaultAgent;
646
665
  };
647
666
  const roleEntries = Object.entries(spec.roles ?? {});
648
- const roles = roleEntries.map(([name, role]: [string, any]) => ({
649
- name,
650
- fromAgent: role.fromAgent,
651
- content: role.prompt ?? "",
652
- maxChars: role.maxChars ?? 8000,
653
- truncated: false,
654
- includedSections: [],
655
- excludedSections: [],
656
- }));
667
+ const roles = await Promise.all(
668
+ roleEntries.map(async ([name, role]: [string, any]) => {
669
+ const sourceAgent = role.fromAgent
670
+ ? await loadWorkflowAgent(
671
+ role.fromAgent,
672
+ options.cwd,
673
+ agentCache,
674
+ `$.roles.${name}.fromAgent`,
675
+ )
676
+ : undefined;
677
+ return compileRole(name, role, sourceAgent);
678
+ }),
679
+ );
657
680
  const roleText = roles.length
658
681
  ? `# Role Context\n\n${roles.map((r) => `## Role: ${r.name}\n${r.content}`).join("\n\n")}`
659
682
  : "";
@@ -665,9 +688,9 @@ async function compileArtifactGraphPlan(
665
688
  Object.keys(workflowInput).length > 0
666
689
  ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
667
690
  : "";
668
- const defaultModel = options.runtimeDefaults?.model ?? spec.defaults?.model;
669
- const defaultThinking =
670
- options.runtimeDefaults?.thinking ?? spec.defaults?.thinking;
691
+ const runtimeOverrides = options.runtimeOverrides;
692
+ const runtimeDefaults = options.runtimeDefaults;
693
+ const specRuntimeDefaults = runtimeSettings(spec.defaults);
671
694
  const tasks: any[] = [];
672
695
  const stageRecords: any[] = [];
673
696
  const issues: ValidationIssue[] = [];
@@ -719,6 +742,22 @@ async function compileArtifactGraphPlan(
719
742
  dynamicToolPath,
720
743
  );
721
744
  const dynamicToolSelection = filterToolSelection(rawDynamicToolSelection);
745
+ const requestedRuntime = selectWorkflowRuntime(
746
+ runtimeOverrides,
747
+ runtimeSettings(stage),
748
+ runtimeDefaults,
749
+ specRuntimeDefaults,
750
+ );
751
+ const resolvedDynamicRuntime = await resolveWorkflowRuntime(
752
+ requestedRuntime,
753
+ {
754
+ taskKey: key,
755
+ stageId: stage.id,
756
+ taskId,
757
+ agent: "dynamic",
758
+ },
759
+ { availableModels: options.availableModels },
760
+ );
722
761
  const dynamicTask = buildDynamicTask(
723
762
  stage,
724
763
  taskId,
@@ -729,24 +768,22 @@ async function compileArtifactGraphPlan(
729
768
  specDir,
730
769
  workflowInputText,
731
770
  options.task,
732
- defaultModel,
733
- defaultThinking,
734
- overrides,
735
- );
736
- const resolvedDynamicRuntime = await resolveWorkflowRuntime(
737
- { model: defaultModel, thinking: defaultThinking },
771
+ resolvedDynamicRuntime,
738
772
  {
739
- taskKey: key,
740
- stageId: stage.id,
741
- taskId,
742
- agent: "dynamic",
773
+ runtimeOverrides,
774
+ runtimeDefaults,
775
+ specRuntimeDefaults,
776
+ stageRuntime: runtimeSettings(stage),
743
777
  },
744
- { availableModels: options.availableModels },
778
+ overrides,
745
779
  );
746
780
  dynamicTask.runtime = {
747
781
  ...dynamicTask.runtime,
748
782
  ...resolvedDynamicRuntime,
749
783
  };
784
+ if (options.availableModels?.length) {
785
+ dynamicTask.dynamic.availableModels = options.availableModels;
786
+ }
750
787
  if (dynamicToolSelection.tools || dynamicToolSelection.toolProviders) {
751
788
  dynamicTask.runtime = {
752
789
  ...dynamicTask.runtime,
@@ -823,15 +860,12 @@ async function compileArtifactGraphPlan(
823
860
  validateToolSubset(toolSelection.tools, stageAgent, issues, toolPath);
824
861
  validateDelegationBoundary(toolSelection.tools, issues, toolPath);
825
862
  const filteredToolSelection = filterToolSelection(toolSelection);
826
- // Explicit runtime overrides outrank stage pins; spec defaults fill last.
827
- const requestedRuntime = {
828
- model:
829
- options.runtimeDefaults?.model ?? stage.model ?? spec.defaults?.model,
830
- thinking:
831
- options.runtimeDefaults?.thinking ??
832
- stage.thinking ??
833
- spec.defaults?.thinking,
834
- };
863
+ const requestedRuntime = selectWorkflowRuntime(
864
+ runtimeOverrides,
865
+ runtimeSettings(stage),
866
+ runtimeDefaults,
867
+ specRuntimeDefaults,
868
+ );
835
869
  const resolvedRuntime = await resolveWorkflowRuntime(
836
870
  requestedRuntime,
837
871
  {
@@ -1208,12 +1242,34 @@ async function compileArtifactGraphPlan(
1208
1242
  tasks,
1209
1243
  warnings,
1210
1244
  budget: {
1211
- models: defaultModel ? [{ model: defaultModel }] : [],
1245
+ models: budgetModelRows(tasks),
1212
1246
  unratedModels: [],
1213
1247
  },
1214
1248
  };
1215
1249
  }
1216
1250
 
1251
+ function budgetModelRows(tasks: any[]): Array<{ model: string }> {
1252
+ const models = new Set<string>();
1253
+ for (const task of tasks) {
1254
+ if (typeof task?.runtime?.model === "string" && task.runtime.model.trim()) {
1255
+ models.add(task.runtime.model.trim());
1256
+ }
1257
+ const loop = task?.dynamic?.decisionLoop;
1258
+ if (!loop || typeof loop !== "object") continue;
1259
+ for (const profile of [
1260
+ loop.planner,
1261
+ loop.workerDefaults,
1262
+ loop.verifier,
1263
+ loop.synthesis,
1264
+ ]) {
1265
+ if (typeof profile?.model === "string" && profile.model.trim()) {
1266
+ models.add(profile.model.trim());
1267
+ }
1268
+ }
1269
+ }
1270
+ return [...models].sort().map((model) => ({ model }));
1271
+ }
1272
+
1217
1273
  function isSupportStage(stage: any): boolean {
1218
1274
  return stage?.support !== undefined && stage?.type === undefined;
1219
1275
  }
@@ -1301,8 +1357,13 @@ function buildDynamicTask(
1301
1357
  specDir: string,
1302
1358
  workflowInputText: string,
1303
1359
  runtimeTask: string | undefined,
1304
- defaultModel: string | undefined,
1305
- defaultThinking: ThinkingLevel | undefined,
1360
+ controllerRuntime: WorkflowRuntimeResolutionInput,
1361
+ runtimePriority: {
1362
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1363
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1364
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1365
+ stageRuntime?: WorkflowRuntimeDefaults;
1366
+ },
1306
1367
  overrides: Partial<CompiledTask> & Record<string, unknown>,
1307
1368
  ): any {
1308
1369
  const dynamic = stage.dynamic ?? {};
@@ -1365,8 +1426,7 @@ function buildDynamicTask(
1365
1426
  }
1366
1427
  const decisionLoop = compileDynamicDecisionLoop(
1367
1428
  dynamic.decisionLoop,
1368
- defaultModel,
1369
- defaultThinking,
1429
+ runtimePriority,
1370
1430
  );
1371
1431
 
1372
1432
  return {
@@ -1386,8 +1446,7 @@ function buildDynamicTask(
1386
1446
  explicitWorktreePolicy: false,
1387
1447
  runtime: {
1388
1448
  approvalMode: "non-interactive",
1389
- model: defaultModel,
1390
- thinking: defaultThinking,
1449
+ ...controllerRuntime,
1391
1450
  maxRuntimeMs:
1392
1451
  dynamic.budget?.maxRuntimeMs ?? DEFAULT_DYNAMIC_MAX_RUNTIME_MS,
1393
1452
  },
@@ -1431,6 +1490,9 @@ function buildDynamicTask(
1431
1490
  helpers,
1432
1491
  workflows,
1433
1492
  ...(decisionLoop ? { decisionLoop } : {}),
1493
+ ...(runtimePriority.runtimeOverrides
1494
+ ? { runtimeOverrides: runtimePriority.runtimeOverrides }
1495
+ : {}),
1434
1496
  },
1435
1497
  ...overrides,
1436
1498
  };
@@ -1438,8 +1500,12 @@ function buildDynamicTask(
1438
1500
 
1439
1501
  function compileDynamicDecisionLoop(
1440
1502
  value: unknown,
1441
- defaultModel?: string,
1442
- defaultThinking?: ThinkingLevel,
1503
+ runtimePriority: {
1504
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1505
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1506
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1507
+ stageRuntime?: WorkflowRuntimeDefaults;
1508
+ },
1443
1509
  ): any | undefined {
1444
1510
  if (!isPlainRecord(value)) return undefined;
1445
1511
  const allowedToolSelection = filterToolSelection(
@@ -1455,25 +1521,18 @@ function compileDynamicDecisionLoop(
1455
1521
  recordValue(value.stateIndex, "requiredFindingIds"),
1456
1522
  );
1457
1523
  return {
1458
- planner: compileDynamicDecisionLoopProfile(
1459
- value.planner,
1460
- defaultModel,
1461
- defaultThinking,
1462
- ),
1524
+ planner: compileDynamicDecisionLoopProfile(value.planner, runtimePriority),
1463
1525
  workerDefaults: compileDynamicDecisionLoopProfile(
1464
1526
  value.workerDefaults,
1465
- defaultModel,
1466
- defaultThinking,
1527
+ runtimePriority,
1467
1528
  ),
1468
1529
  verifier: compileDynamicDecisionLoopProfile(
1469
1530
  value.verifier,
1470
- defaultModel,
1471
- defaultThinking,
1531
+ runtimePriority,
1472
1532
  ),
1473
1533
  synthesis: compileDynamicDecisionLoopProfile(
1474
1534
  value.synthesis,
1475
- defaultModel,
1476
- defaultThinking,
1535
+ runtimePriority,
1477
1536
  ),
1478
1537
  allowedAgents: stringArray(value.allowedAgents),
1479
1538
  ...(allowedToolSelection.tools
@@ -1528,8 +1587,12 @@ function compileDynamicDecisionLoop(
1528
1587
 
1529
1588
  function compileDynamicDecisionLoopProfile(
1530
1589
  value: unknown,
1531
- defaultModel?: string,
1532
- defaultThinking?: ThinkingLevel,
1590
+ runtimePriority: {
1591
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1592
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1593
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1594
+ stageRuntime?: WorkflowRuntimeDefaults;
1595
+ },
1533
1596
  ): any | undefined {
1534
1597
  if (!isPlainRecord(value)) return undefined;
1535
1598
  const toolSelection = filterToolSelection(
@@ -1538,20 +1601,18 @@ function compileDynamicDecisionLoopProfile(
1538
1601
  undefined,
1539
1602
  ),
1540
1603
  );
1541
- const model =
1542
- typeof value.model === "string" && value.model.trim()
1543
- ? value.model.trim()
1544
- : defaultModel;
1545
- const thinking =
1546
- typeof value.thinking === "string" && value.thinking.trim()
1547
- ? value.thinking.trim()
1548
- : defaultThinking;
1604
+ const runtime = selectWorkflowRuntime(
1605
+ runtimePriority.runtimeOverrides,
1606
+ runtimeSettings(value),
1607
+ runtimePriority.stageRuntime,
1608
+ runtimePriority.runtimeDefaults,
1609
+ runtimePriority.specRuntimeDefaults,
1610
+ );
1549
1611
  return {
1550
1612
  ...(typeof value.agent === "string" && value.agent.trim()
1551
1613
  ? { agent: value.agent.trim() }
1552
1614
  : {}),
1553
- ...(model ? { model } : {}),
1554
- ...(thinking ? { thinking } : {}),
1615
+ ...runtime,
1555
1616
  ...(toolSelection.tools ? { tools: toolSelection.tools } : {}),
1556
1617
  ...(toolSelection.toolProviders
1557
1618
  ? { toolProviders: toolSelection.toolProviders }
@@ -283,17 +283,6 @@ export function validateDynamicDecision(
283
283
  };
284
284
  }
285
285
 
286
- export function assertValidDynamicDecision(
287
- value: unknown,
288
- context: DynamicDecisionValidationContext = {},
289
- ): NormalizedDynamicDecision {
290
- const result = validateDynamicDecision(value, context);
291
- if (!result.ok || !result.decision) {
292
- throw new Error(`invalid dynamic decision: ${result.errors.join("; ")}`);
293
- }
294
- return result.decision;
295
- }
296
-
297
286
  export function hashDynamicDecision(value: unknown): string {
298
287
  return createHash("sha256")
299
288
  .update(stableStringify(toJsonNormalizedValue(value)))
@@ -24,6 +24,11 @@ import type {
24
24
  WorkflowRunRecord,
25
25
  WorkflowTaskRunRecord,
26
26
  } from "./types.js";
27
+ import {
28
+ resolveWorkflowRuntime,
29
+ selectWorkflowRuntime,
30
+ type WorkflowModelInfo,
31
+ } from "./workflow-runtime.js";
27
32
 
28
33
  const DYNAMIC_OUTPUT_MAX_DIGEST_CHARS = 1000;
29
34
  const DYNAMIC_DELEGATION_TOOLS = new Set([
@@ -92,6 +97,7 @@ export async function buildDynamicGeneratedCompiledTask(input: {
92
97
  branchId?: string;
93
98
  request: DynamicAgentRequest;
94
99
  dynamic: CompiledDynamicWorkflowTask;
100
+ availableModels?: WorkflowModelInfo[];
95
101
  }): Promise<CompiledTask> {
96
102
  if (input.dynamic.budget.maxAgents <= 0) {
97
103
  throw new Error("dynamic agent budget is exhausted");
@@ -172,6 +178,23 @@ export async function buildDynamicGeneratedCompiledTask(input: {
172
178
  ),
173
179
  ),
174
180
  );
181
+ const selectedRuntime = selectWorkflowRuntime(
182
+ input.dynamic.runtimeOverrides,
183
+ runtimeSettings(input.request),
184
+ runtimeSettings(executionProfile),
185
+ runtimeSettings(input.controllerCompiledTask.runtime),
186
+ runtimeSettings(agentDefinition),
187
+ );
188
+ const resolvedRuntime = await resolveWorkflowRuntime(
189
+ selectedRuntime,
190
+ {
191
+ taskKey: input.generatedSpecId,
192
+ stageId: input.controllerStageId,
193
+ taskId: input.request.id,
194
+ agent: requestedAgent,
195
+ },
196
+ { availableModels: input.availableModels ?? input.dynamic.availableModels },
197
+ );
175
198
  const unknownTools = (tools ?? []).filter(
176
199
  (tool) => effectiveToolClassification(tool, toolProviders) === undefined,
177
200
  );
@@ -253,16 +276,7 @@ export async function buildDynamicGeneratedCompiledTask(input: {
253
276
  explicitWorktreePolicy: requiresWorktree,
254
277
  runtime: {
255
278
  approvalMode: "non-interactive",
256
- model:
257
- input.request.model ??
258
- executionProfile?.model ??
259
- input.controllerCompiledTask.runtime.model ??
260
- agentDefinition.model,
261
- thinking:
262
- input.request.thinking ??
263
- executionProfile?.thinking ??
264
- input.controllerCompiledTask.runtime.thinking ??
265
- agentDefinition.thinking,
279
+ ...resolvedRuntime,
266
280
  tools,
267
281
  ...(toolProviders ? { toolProviders } : {}),
268
282
  maxRuntimeMs:
@@ -419,7 +433,9 @@ function dynamicDecisionLoopProfile(
419
433
  );
420
434
  }
421
435
 
422
- export function isDynamicCompiledTaskPayload(value: unknown): value is CompiledTask {
436
+ export function isDynamicCompiledTaskPayload(
437
+ value: unknown,
438
+ ): value is CompiledTask {
423
439
  return (
424
440
  !!value &&
425
441
  typeof value === "object" &&
@@ -679,7 +695,9 @@ export async function readDynamicGeneratedTaskResult(
679
695
  };
680
696
  }
681
697
 
682
- export function normalizeDynamicAgentRequest(value: unknown): DynamicAgentRequest {
698
+ export function normalizeDynamicAgentRequest(
699
+ value: unknown,
700
+ ): DynamicAgentRequest {
683
701
  if (!value || typeof value !== "object" || Array.isArray(value)) {
684
702
  throw new Error("ctx.agent() request must be an object");
685
703
  }
@@ -730,6 +748,23 @@ function requiredDynamicString(
730
748
  return value.trim();
731
749
  }
732
750
 
751
+ function runtimeSettings(
752
+ value: unknown,
753
+ ): { model?: string; thinking?: ThinkingLevel } | undefined {
754
+ if (!value || typeof value !== "object" || Array.isArray(value))
755
+ return undefined;
756
+ const record = value as Record<string, unknown>;
757
+ const model =
758
+ typeof record.model === "string" && record.model.trim()
759
+ ? record.model.trim()
760
+ : undefined;
761
+ const thinking =
762
+ typeof record.thinking === "string" && record.thinking.trim()
763
+ ? (record.thinking.trim() as ThinkingLevel)
764
+ : undefined;
765
+ return model || thinking ? { model, thinking } : undefined;
766
+ }
767
+
733
768
  function optionalDynamicString(
734
769
  value: unknown,
735
770
  field: string,
@@ -44,7 +44,3 @@ export function isTerminalDynamicOutputProfile(
44
44
  ): value is (typeof DYNAMIC_TERMINAL_OUTPUT_PROFILES)[number] {
45
45
  return typeof value === "string" && TERMINAL_OUTPUT_PROFILE_SET.has(value);
46
46
  }
47
-
48
- export function dynamicOutputProfileValues(): string[] {
49
- return [...DYNAMIC_OUTPUT_PROFILES];
50
- }