@agwab/pi-workflow 0.1.2 → 0.2.1

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 (41) hide show
  1. package/README.md +9 -13
  2. package/dist/compiler.d.ts +5 -5
  3. package/dist/compiler.js +82 -24
  4. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  5. package/dist/dynamic-generated-task-runtime.js +21 -8
  6. package/dist/engine.d.ts +6 -5
  7. package/dist/engine.js +39 -54
  8. package/dist/extension.js +211 -24
  9. package/dist/store.d.ts +3 -1
  10. package/dist/store.js +135 -38
  11. package/dist/subagent-backend.d.ts +4 -0
  12. package/dist/subagent-backend.js +128 -4
  13. package/dist/types.d.ts +5 -0
  14. package/dist/workflow-progress-health.d.ts +37 -0
  15. package/dist/workflow-progress-health.js +296 -0
  16. package/dist/workflow-runtime.d.ts +8 -0
  17. package/dist/workflow-runtime.js +63 -10
  18. package/dist/workflow-view.d.ts +2 -0
  19. package/dist/workflow-view.js +97 -18
  20. package/dist/workflow-web-source.js +32 -14
  21. package/docs/usage.md +12 -1
  22. package/package.json +6 -6
  23. package/src/compiler.ts +136 -41
  24. package/src/dynamic-generated-task-runtime.ts +47 -12
  25. package/src/engine.ts +55 -100
  26. package/src/extension.ts +270 -34
  27. package/src/store.ts +180 -44
  28. package/src/subagent-backend.ts +170 -6
  29. package/src/types.ts +10 -0
  30. package/src/workflow-progress-health.ts +461 -0
  31. package/src/workflow-runtime.ts +85 -13
  32. package/src/workflow-view.ts +186 -41
  33. package/src/workflow-web-source.ts +192 -69
  34. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  35. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  36. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  37. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  38. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  39. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  40. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  41. package/workflows/deep-research/spec.json +41 -11
package/README.md CHANGED
@@ -64,6 +64,8 @@ If you want deterministic manual control, use the slash command form:
64
64
  /workflow run deep-research "Research this repository and summarize the architecture tradeoffs."
65
65
  ```
66
66
 
67
+ For opt-in lower-latency runs, add `--thinking low`; defaults remain conservative pending holdout evidence. See [`docs/usage.md`](./docs/usage.md).
68
+
67
69
  For a one-off adaptive workflow that should plan, fan out, and synthesize without choosing a saved workflow, use:
68
70
 
69
71
  ```text
@@ -167,22 +169,16 @@ Workflow definitions compose a small set of stage patterns and graph shapes.
167
169
 
168
170
  ![Core workflow stage shapes: single, foreach, reduce, loop, dag, and dynamic](./docs/assets/readme/stage-types.png)
169
171
 
170
- Parallel execution is a graph shape, not a stage type: model parallel branches as multiple roots or with `after: []`. Support helpers are declared with a `support` object, not a stage `type`.
171
-
172
- Dynamic workflows are the advanced form of adaptive orchestration. A JSON `type: "dynamic"` stage points at trusted bundle-local `.mjs` controller code; that controller can add official workflow tasks, call helpers, or run nested workflows while preserving replayable run state. See [`docs/usage.md`](./docs/usage.md) for approval, detach, helper retry, nested workflow, and cache details.
173
-
174
- New workflows should prefer `workflow_web_search`, `workflow_web_fetch_source`, and `workflow_web_source_read`: search returns compact candidates, fetch returns a source card with an opaque `sourceRef`, and source-read retrieves narrow evidence snippets. Preserve `sourceRef` through workflow outputs; use `urls: [...]` or `sources: [...]` to batch several source fetches, use `queries: [...]` or `reads: [...]` to batch several snippets from one sourceRef, and use `claim` + distinctive `terms` to get candidate quote windows with match metadata when the exact quote is unknown. Normalized web sources are cached under `.pi/workflows/<run-id>/web-source-cache/` without exposing cache paths to agents; same-URL fetches and deterministic terminal failures are coordinated across parallel workers. Legacy `fetch_content` calls still use `.pi/workflows/<run-id>/source-cache/fetch-content/`; set `PI_WORKFLOW_FETCH_CONTENT_CACHE=0` to opt out of that legacy cache.
175
-
176
172
  ## Predefined workflows
177
173
 
178
- The package includes a small starter set. These are practical defaults and authoring examples, not a complete workflow catalog.
174
+ The package includes four bundled workflows for common research and review jobs. They are runnable defaults and authoring examples, not a complete workflow catalog.
179
175
 
180
- | Workflow | Use when |
181
- |---|---|
182
- | `deep-research` | Grounded answers or summaries based on source material. |
183
- | `deep-review` | Careful code or design review from multiple angles. |
184
- | `spec-review` | Checks that requirements, API specs, or contracts are reflected in implementation and tests. |
185
- | `impact-review` | Pre-merge or pre-release risk review across affected areas, tests, and docs. |
176
+ | Workflow | Best for | What it does |
177
+ |---|---|---|
178
+ | `deep-research` | Deep, source-grounded research when breadth, verification, and cited recommendations matter. | Plans research questions by depth, fans out question-level research, normalizes and ranks claims, verifies selected claims against evidence, and renders an audited executive handoff. |
179
+ | `deep-review` | Code or design review when one reviewer pass is not enough. | Triage selects review lenses, reviewers produce findings, a deterministic helper deduplicates them, a challenge pass tests the surviving findings, a deterministic helper partitions verdicts, and the final report keeps only evidence-backed issues. |
180
+ | `spec-review` | Requirements-to-implementation traceability for an existing spec, API contract, or acceptance criteria. | Extracts testable requirements, maps implementation and tests, verifies candidate gaps, and reports which requirements are covered, missing, ambiguous, or need human judgment. |
181
+ | `impact-review` | Side-effect and risk review for proposed or applied changes. | Maps change scope and affected surfaces, analyzes contract, state/data, validation, docs, security, and performance impact, then joins those lenses into likely regressions, missing checks, and next actions. |
186
182
 
187
183
  ![Deep research workflow flow](./docs/assets/readme/deep-research-flow.png)
188
184
 
@@ -1,13 +1,13 @@
1
- import { type ArtifactGraphWorkflowSpec, type ThinkingLevel } from "./types.js";
1
+ import { type ArtifactGraphWorkflowSpec } from "./types.js";
2
+ import { type WorkflowModelInfo, type WorkflowRuntimeDefaults } from "./workflow-runtime.js";
2
3
  interface CompileOptions {
3
4
  cwd: string;
4
5
  specPath?: string;
6
+ availableModels?: WorkflowModelInfo[];
5
7
  }
6
8
  export declare function compileWorkflow(spec: ArtifactGraphWorkflowSpec, options: CompileOptions & {
7
9
  task?: string;
8
- runtimeDefaults?: {
9
- model?: string;
10
- thinking?: ThinkingLevel;
11
- };
10
+ runtimeOverrides?: WorkflowRuntimeDefaults;
11
+ runtimeDefaults?: WorkflowRuntimeDefaults;
12
12
  }): Promise<any>;
13
13
  export {};
package/dist/compiler.js CHANGED
@@ -4,6 +4,7 @@ import { loadAgentByName } from "./agents.js";
4
4
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
5
5
  import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, resolveToolSelection, TOOL_NAME_PATTERN, toolAllowedByAuthorityCeiling, toolNameForSpec, } from "./tool-metadata.js";
6
6
  import { WorkflowValidationError, WORKFLOW_RUN_TYPE, } from "./types.js";
7
+ import { resolveWorkflowRuntime, selectWorkflowRuntime, } from "./workflow-runtime.js";
7
8
  const DELEGATION_TOOLS = new Set([
8
9
  "skill_test_subagent",
9
10
  "workflow",
@@ -412,6 +413,17 @@ async function collectForeachPathWarnings(stages, specDir) {
412
413
  }
413
414
  return warnings;
414
415
  }
416
+ function runtimeSettings(value) {
417
+ if (!isPlainRecord(value))
418
+ return undefined;
419
+ const model = typeof value.model === "string" && value.model.trim()
420
+ ? value.model.trim()
421
+ : undefined;
422
+ const thinking = typeof value.thinking === "string" && value.thinking.trim()
423
+ ? value.thinking.trim()
424
+ : undefined;
425
+ return model || thinking ? { model, thinking } : undefined;
426
+ }
415
427
  async function compileArtifactGraphPlan(spec, options) {
416
428
  const stages = spec.stages;
417
429
  if (!Array.isArray(stages)) {
@@ -449,8 +461,9 @@ async function compileArtifactGraphPlan(spec, options) {
449
461
  Object.keys(workflowInput).length > 0
450
462
  ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
451
463
  : "";
452
- const defaultModel = options.runtimeDefaults?.model ?? spec.defaults?.model;
453
- const defaultThinking = options.runtimeDefaults?.thinking ?? spec.defaults?.thinking;
464
+ const runtimeOverrides = options.runtimeOverrides;
465
+ const runtimeDefaults = options.runtimeDefaults;
466
+ const specRuntimeDefaults = runtimeSettings(spec.defaults);
454
467
  const tasks = [];
455
468
  const stageRecords = [];
456
469
  const issues = [];
@@ -474,7 +487,26 @@ async function compileArtifactGraphPlan(spec, options) {
474
487
  : `$.artifactGraph.stages.${jsonKey(stage.id)}.dynamic`;
475
488
  validateDelegationBoundary(rawDynamicToolSelection.tools, issues, dynamicToolPath);
476
489
  const dynamicToolSelection = filterToolSelection(rawDynamicToolSelection);
477
- const dynamicTask = buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, options.cwd, specDir, workflowInputText, options.task, defaultModel, defaultThinking, overrides);
490
+ const requestedRuntime = selectWorkflowRuntime(runtimeOverrides, runtimeSettings(stage), runtimeDefaults, specRuntimeDefaults);
491
+ const resolvedDynamicRuntime = await resolveWorkflowRuntime(requestedRuntime, {
492
+ taskKey: key,
493
+ stageId: stage.id,
494
+ taskId,
495
+ agent: "dynamic",
496
+ }, { availableModels: options.availableModels });
497
+ const dynamicTask = buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, options.cwd, specDir, workflowInputText, options.task, resolvedDynamicRuntime, {
498
+ runtimeOverrides,
499
+ runtimeDefaults,
500
+ specRuntimeDefaults,
501
+ stageRuntime: runtimeSettings(stage),
502
+ }, overrides);
503
+ dynamicTask.runtime = {
504
+ ...dynamicTask.runtime,
505
+ ...resolvedDynamicRuntime,
506
+ };
507
+ if (options.availableModels?.length) {
508
+ dynamicTask.dynamic.availableModels = options.availableModels;
509
+ }
478
510
  if (dynamicToolSelection.tools || dynamicToolSelection.toolProviders) {
479
511
  dynamicTask.runtime = {
480
512
  ...dynamicTask.runtime,
@@ -528,10 +560,18 @@ async function compileArtifactGraphPlan(spec, options) {
528
560
  validateToolSubset(toolSelection.tools, stageAgent, issues, toolPath);
529
561
  validateDelegationBoundary(toolSelection.tools, issues, toolPath);
530
562
  const filteredToolSelection = filterToolSelection(toolSelection);
563
+ const requestedRuntime = selectWorkflowRuntime(runtimeOverrides, runtimeSettings(stage), runtimeDefaults, specRuntimeDefaults);
564
+ const resolvedRuntime = await resolveWorkflowRuntime(requestedRuntime, {
565
+ taskKey: key,
566
+ stageId: stage.id,
567
+ taskId,
568
+ agent: stageAgentName,
569
+ }, {
570
+ availableModels: options.availableModels,
571
+ });
531
572
  const runtime = {
532
573
  approvalMode: stage.approvalMode ?? spec.defaults?.approvalMode ?? "non-interactive",
533
- model: stage.model ?? defaultModel,
534
- thinking: stage.thinking ?? defaultThinking,
574
+ ...resolvedRuntime,
535
575
  tools: filteredToolSelection.tools,
536
576
  ...(filteredToolSelection.toolProviders
537
577
  ? { toolProviders: filteredToolSelection.toolProviders }
@@ -800,11 +840,33 @@ async function compileArtifactGraphPlan(spec, options) {
800
840
  tasks,
801
841
  warnings,
802
842
  budget: {
803
- models: defaultModel ? [{ model: defaultModel }] : [],
843
+ models: budgetModelRows(tasks),
804
844
  unratedModels: [],
805
845
  },
806
846
  };
807
847
  }
848
+ function budgetModelRows(tasks) {
849
+ const models = new Set();
850
+ for (const task of tasks) {
851
+ if (typeof task?.runtime?.model === "string" && task.runtime.model.trim()) {
852
+ models.add(task.runtime.model.trim());
853
+ }
854
+ const loop = task?.dynamic?.decisionLoop;
855
+ if (!loop || typeof loop !== "object")
856
+ continue;
857
+ for (const profile of [
858
+ loop.planner,
859
+ loop.workerDefaults,
860
+ loop.verifier,
861
+ loop.synthesis,
862
+ ]) {
863
+ if (typeof profile?.model === "string" && profile.model.trim()) {
864
+ models.add(profile.model.trim());
865
+ }
866
+ }
867
+ }
868
+ return [...models].sort().map((model) => ({ model }));
869
+ }
808
870
  function isSupportStage(stage) {
809
871
  return stage?.support !== undefined && stage?.type === undefined;
810
872
  }
@@ -864,7 +926,7 @@ function buildSupportTask(stage, taskId, key, prompt, dependencyKeys, cwd, workf
864
926
  ...overrides,
865
927
  };
866
928
  }
867
- function buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, cwd, specDir, workflowInputText, runtimeTask, defaultModel, defaultThinking, overrides) {
929
+ function buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, cwd, specDir, workflowInputText, runtimeTask, controllerRuntime, runtimePriority, overrides) {
868
930
  const dynamic = stage.dynamic ?? {};
869
931
  const uses = String(dynamic.uses);
870
932
  const normalizedPrompt = String(prompt ?? "").replace(/\$\{item\}/g, "the relevant item from the dependency context");
@@ -917,7 +979,7 @@ function buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, cwd, specD
917
979
  usesPath: resolve(specDir, String(workflow.uses)),
918
980
  };
919
981
  }
920
- const decisionLoop = compileDynamicDecisionLoop(dynamic.decisionLoop, defaultModel, defaultThinking);
982
+ const decisionLoop = compileDynamicDecisionLoop(dynamic.decisionLoop, runtimePriority);
921
983
  return {
922
984
  key,
923
985
  id: key,
@@ -935,8 +997,7 @@ function buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, cwd, specD
935
997
  explicitWorktreePolicy: false,
936
998
  runtime: {
937
999
  approvalMode: "non-interactive",
938
- model: defaultModel,
939
- thinking: defaultThinking,
1000
+ ...controllerRuntime,
940
1001
  maxRuntimeMs: dynamic.budget?.maxRuntimeMs ?? DEFAULT_DYNAMIC_MAX_RUNTIME_MS,
941
1002
  },
942
1003
  safety: {
@@ -974,21 +1035,24 @@ function buildDynamicTask(stage, taskId, key, prompt, dependencyKeys, cwd, specD
974
1035
  helpers,
975
1036
  workflows,
976
1037
  ...(decisionLoop ? { decisionLoop } : {}),
1038
+ ...(runtimePriority.runtimeOverrides
1039
+ ? { runtimeOverrides: runtimePriority.runtimeOverrides }
1040
+ : {}),
977
1041
  },
978
1042
  ...overrides,
979
1043
  };
980
1044
  }
981
- function compileDynamicDecisionLoop(value, defaultModel, defaultThinking) {
1045
+ function compileDynamicDecisionLoop(value, runtimePriority) {
982
1046
  if (!isPlainRecord(value))
983
1047
  return undefined;
984
1048
  const allowedToolSelection = filterToolSelection(resolveToolSelection([Array.isArray(value.allowedTools) ? value.allowedTools : undefined], undefined));
985
1049
  const maxFindings = positiveInteger(recordValue(value.stateIndex, "maxFindings"));
986
1050
  const deprecatedRequiredFindingIds = stringArray(recordValue(value.stateIndex, "requiredFindingIds"));
987
1051
  return {
988
- planner: compileDynamicDecisionLoopProfile(value.planner, defaultModel, defaultThinking),
989
- workerDefaults: compileDynamicDecisionLoopProfile(value.workerDefaults, defaultModel, defaultThinking),
990
- verifier: compileDynamicDecisionLoopProfile(value.verifier, defaultModel, defaultThinking),
991
- synthesis: compileDynamicDecisionLoopProfile(value.synthesis, defaultModel, defaultThinking),
1052
+ planner: compileDynamicDecisionLoopProfile(value.planner, runtimePriority),
1053
+ workerDefaults: compileDynamicDecisionLoopProfile(value.workerDefaults, runtimePriority),
1054
+ verifier: compileDynamicDecisionLoopProfile(value.verifier, runtimePriority),
1055
+ synthesis: compileDynamicDecisionLoopProfile(value.synthesis, runtimePriority),
992
1056
  allowedAgents: stringArray(value.allowedAgents),
993
1057
  ...(allowedToolSelection.tools
994
1058
  ? { allowedTools: allowedToolSelection.tools }
@@ -1029,22 +1093,16 @@ function compileDynamicDecisionLoop(value, defaultModel, defaultThinking) {
1029
1093
  },
1030
1094
  };
1031
1095
  }
1032
- function compileDynamicDecisionLoopProfile(value, defaultModel, defaultThinking) {
1096
+ function compileDynamicDecisionLoopProfile(value, runtimePriority) {
1033
1097
  if (!isPlainRecord(value))
1034
1098
  return undefined;
1035
1099
  const toolSelection = filterToolSelection(resolveToolSelection([Array.isArray(value.tools) ? value.tools : undefined], undefined));
1036
- const model = typeof value.model === "string" && value.model.trim()
1037
- ? value.model.trim()
1038
- : defaultModel;
1039
- const thinking = typeof value.thinking === "string" && value.thinking.trim()
1040
- ? value.thinking.trim()
1041
- : defaultThinking;
1100
+ const runtime = selectWorkflowRuntime(runtimePriority.runtimeOverrides, runtimeSettings(value), runtimePriority.stageRuntime, runtimePriority.runtimeDefaults, runtimePriority.specRuntimeDefaults);
1042
1101
  return {
1043
1102
  ...(typeof value.agent === "string" && value.agent.trim()
1044
1103
  ? { agent: value.agent.trim() }
1045
1104
  : {}),
1046
- ...(model ? { model } : {}),
1047
- ...(thinking ? { thinking } : {}),
1105
+ ...runtime,
1048
1106
  ...(toolSelection.tools ? { tools: toolSelection.tools } : {}),
1049
1107
  ...(toolSelection.toolProviders
1050
1108
  ? { toolProviders: toolSelection.toolProviders }
@@ -1,4 +1,5 @@
1
1
  import type { CompiledDynamicWorkflowTask, CompiledTask, CompiledWorkflow, ThinkingLevel, WorkflowRunRecord, WorkflowTaskRunRecord } from "./types.js";
2
+ import { type WorkflowModelInfo } from "./workflow-runtime.js";
2
3
  export interface DynamicArtifactInput {
3
4
  kind: "workflow-artifact-ref";
4
5
  name: string;
@@ -41,6 +42,7 @@ export declare function buildDynamicGeneratedCompiledTask(input: {
41
42
  branchId?: string;
42
43
  request: DynamicAgentRequest;
43
44
  dynamic: CompiledDynamicWorkflowTask;
45
+ availableModels?: WorkflowModelInfo[];
44
46
  }): Promise<CompiledTask>;
45
47
  export declare function isDynamicCompiledTaskPayload(value: unknown): value is CompiledTask;
46
48
  export declare function assertDynamicGeneratedMetadataMatches(compiledTask: CompiledTask, expected: {
@@ -6,6 +6,7 @@ import { readOrRebuildDynamicState } from "./dynamic-state.js";
6
6
  import { sanitizeTaskId } from "./engine-run-graph.js";
7
7
  import { fromProjectPath, isTerminalTaskStatus, readJson } from "./store.js";
8
8
  import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, toolAllowedByAuthorityCeiling, } from "./tool-metadata.js";
9
+ import { resolveWorkflowRuntime, selectWorkflowRuntime, } from "./workflow-runtime.js";
9
10
  const DYNAMIC_OUTPUT_MAX_DIGEST_CHARS = 1000;
10
11
  const DYNAMIC_DELEGATION_TOOLS = new Set([
11
12
  "skill_test_subagent",
@@ -69,6 +70,13 @@ export async function buildDynamicGeneratedCompiledTask(input) {
69
70
  }
70
71
  const toolProviders = executionProfile?.toolProviders ??
71
72
  providersForSelectedTools(tools, new Map(Object.entries(input.controllerCompiledTask.runtime.toolProviders ?? {})));
73
+ const selectedRuntime = selectWorkflowRuntime(input.dynamic.runtimeOverrides, runtimeSettings(input.request), runtimeSettings(executionProfile), runtimeSettings(input.controllerCompiledTask.runtime), runtimeSettings(agentDefinition));
74
+ const resolvedRuntime = await resolveWorkflowRuntime(selectedRuntime, {
75
+ taskKey: input.generatedSpecId,
76
+ stageId: input.controllerStageId,
77
+ taskId: input.request.id,
78
+ agent: requestedAgent,
79
+ }, { availableModels: input.availableModels ?? input.dynamic.availableModels });
72
80
  const unknownTools = (tools ?? []).filter((tool) => effectiveToolClassification(tool, toolProviders) === undefined);
73
81
  if (unknownTools.length > 0) {
74
82
  throw new Error(`dynamic agent requested tools without trusted classification metadata: ${unknownTools.join(", ")}`);
@@ -135,14 +143,7 @@ export async function buildDynamicGeneratedCompiledTask(input) {
135
143
  explicitWorktreePolicy: requiresWorktree,
136
144
  runtime: {
137
145
  approvalMode: "non-interactive",
138
- model: input.request.model ??
139
- executionProfile?.model ??
140
- input.controllerCompiledTask.runtime.model ??
141
- agentDefinition.model,
142
- thinking: input.request.thinking ??
143
- executionProfile?.thinking ??
144
- input.controllerCompiledTask.runtime.thinking ??
145
- agentDefinition.thinking,
146
+ ...resolvedRuntime,
146
147
  tools,
147
148
  ...(toolProviders ? { toolProviders } : {}),
148
149
  maxRuntimeMs: input.request.maxRuntimeMs ??
@@ -461,6 +462,18 @@ function requiredDynamicString(value, field, api = "ctx.agent()") {
461
462
  }
462
463
  return value.trim();
463
464
  }
465
+ function runtimeSettings(value) {
466
+ if (!value || typeof value !== "object" || Array.isArray(value))
467
+ return undefined;
468
+ const record = value;
469
+ const model = typeof record.model === "string" && record.model.trim()
470
+ ? record.model.trim()
471
+ : undefined;
472
+ const thinking = typeof record.thinking === "string" && record.thinking.trim()
473
+ ? record.thinking.trim()
474
+ : undefined;
475
+ return model || thinking ? { model, thinking } : undefined;
476
+ }
464
477
  function optionalDynamicString(value, field) {
465
478
  if (value === undefined)
466
479
  return undefined;
package/dist/engine.d.ts CHANGED
@@ -1,20 +1,21 @@
1
+ import { type WorkflowModelInfo, type WorkflowRuntimeDefaults } from "./workflow-runtime.js";
1
2
  import { type DynamicWorkflowUi } from "./dynamic-controller-policy.js";
2
- import { type CompiledWorkflow, type ThinkingLevel, type WorkflowRunRecord } from "./types.js";
3
+ import { type CompiledWorkflow, type WorkflowRunRecord } from "./types.js";
3
4
  export { buildRunSourceContext } from "./workflow-source-context-runtime.js";
4
5
  export { evaluateLoopUntilCondition } from "./loop-runtime.js";
5
6
  export type { DynamicWorkflowUi } from "./dynamic-controller-policy.js";
6
7
  export interface WorkflowRunOptions {
7
8
  task?: string;
8
- runtimeDefaults?: {
9
- model?: string;
10
- thinking?: ThinkingLevel;
11
- };
9
+ runtimeOverrides?: WorkflowRuntimeDefaults;
10
+ runtimeDefaults?: WorkflowRuntimeDefaults;
11
+ availableModels?: WorkflowModelInfo[];
12
12
  dynamicUi?: DynamicWorkflowUi;
13
13
  runId?: string;
14
14
  parentRunId?: string;
15
15
  }
16
16
  interface WorkflowScheduleOptions {
17
17
  dynamicUi?: DynamicWorkflowUi;
18
+ availableModels?: WorkflowModelInfo[];
18
19
  }
19
20
  export declare function runWorkflowSpec(specPath: string, cwd: string, options?: WorkflowRunOptions): Promise<WorkflowRunRecord>;
20
21
  export declare function runDynamicTask(cwd: string, options?: WorkflowRunOptions): Promise<WorkflowRunRecord>;
package/dist/engine.js CHANGED
@@ -1,16 +1,16 @@
1
- import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, extname, join, resolve, } from "node:path";
1
+ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, join, resolve } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { compileWorkflow } from "./compiler.js";
6
6
  import { loadWorkflowSpec } from "./schema.js";
7
- import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
7
+ import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
8
8
  import { resolveWorkflowBackend } from "./backend.js";
9
9
  import { ensureManagedWorktree } from "./worktree.js";
10
10
  import { resolveWorkflowHelperRef } from "./workflow-helpers.js";
11
11
  import { buildAvailableToolView } from "./tool-metadata.js";
12
12
  import { workflowBundleFingerprint, workflowBundleSpecPath, } from "./workflow-source-context-runtime.js";
13
- import { readSimpleJsonPath } from "./workflow-runtime.js";
13
+ import { readSimpleJsonPath, } from "./workflow-runtime.js";
14
14
  import { dynamicRunDir, hashDynamicRequest, readDynamicEvents, } from "./dynamic-events.js";
15
15
  import { ensureDynamicControllerInitialized, readOrRebuildDynamicState, recordDynamicControllerPhase, recordDynamicControllerStatus, recordDynamicEventAndUpdateState, } from "./dynamic-state.js";
16
16
  import { DynamicControllerBudgetBlocked, DynamicControllerNestedApprovalBlocked, DynamicControllerSuspended, } from "./dynamic-controller-errors.js";
@@ -21,7 +21,6 @@ import { normalizeDynamicFanoutPlanRequest, runDynamicDecisionLoopStatusPersistC
21
21
  import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
22
22
  import { reconcileLoopTaskMaterialization, scheduleLoop, } from "./loop-runtime.js";
23
23
  import { executeSupportTask, normalizeDynamicControllerOutput, prepareArtifactGraphRetryTask, prepareDagTask, readArtifactGraphControl, readArtifactGraphSupportSources, readSupportSources, writeArtifactGraphDynamicResult, } from "./artifact-graph-runtime.js";
24
- import { isDynamicOutputProfile, } from "./dynamic-profiles.js";
25
24
  import { DIRECT_DYNAMIC_RUNTIME_VERSION, ensureDirectDynamicRuntimeBundle, } from "./dynamic-runtime-bundle.js";
26
25
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
27
26
  export { buildRunSourceContext } from "./workflow-source-context-runtime.js";
@@ -37,6 +36,7 @@ const DYNAMIC_CONTROLLER_ENGINE_CAPABILITIES = Object.freeze({
37
36
  });
38
37
  const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE = "incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
39
38
  const supervisorTimers = new Map();
39
+ const supervisorRunMtimes = new Map();
40
40
  export async function runWorkflowSpec(specPath, cwd, options = {}) {
41
41
  const loaded = await loadWorkflowSpec(specPath, cwd);
42
42
  return runLoadedWorkflowSpec(cwd, loaded.specPath, loaded.spec, options);
@@ -62,7 +62,9 @@ async function runLoadedWorkflowSpec(cwd, specPath, spec, options, provenance) {
62
62
  cwd,
63
63
  specPath,
64
64
  task: options.task,
65
+ runtimeOverrides: options.runtimeOverrides,
65
66
  runtimeDefaults: options.runtimeDefaults,
67
+ availableModels: options.availableModels,
66
68
  });
67
69
  const { run } = await createRunRecord(cwd, compiled, specPath, {
68
70
  runId: options.runId,
@@ -75,11 +77,14 @@ async function runLoadedWorkflowSpec(cwd, specPath, spec, options, provenance) {
75
77
  await writeStaticRunArtifacts(cwd, run, compiled, spec);
76
78
  await writeRunRecord(cwd, run);
77
79
  });
78
- const scheduled = (await scheduleRun(cwd, run.runId, compiled, {
80
+ const scheduleOptions = {
79
81
  dynamicUi: options.dynamicUi,
80
- })) ?? (await readRunRecord(cwd, run.runId));
82
+ availableModels: options.availableModels,
83
+ };
84
+ const scheduled = (await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
85
+ (await readRunRecord(cwd, run.runId));
81
86
  if (scheduled.status === "running")
82
- watchRun(cwd, scheduled.runId, { dynamicUi: options.dynamicUi });
87
+ watchRun(cwd, scheduled.runId, scheduleOptions);
83
88
  return scheduled;
84
89
  }
85
90
  export async function refreshRun(cwd, runIdOrPrefix) {
@@ -167,15 +172,26 @@ export function watchRun(cwd, runId, options = {}) {
167
172
  return;
168
173
  const timer = setInterval(() => {
169
174
  void (async () => {
175
+ const previousMtime = supervisorRunMtimes.get(key);
176
+ const beforeMtime = await readRunMtimeMs(cwd, runId);
170
177
  const refreshed = await refreshRun(cwd, runId);
178
+ const afterMtime = await readRunMtimeMs(cwd, runId);
179
+ const currentMtime = afterMtime ?? beforeMtime;
180
+ if (currentMtime !== undefined)
181
+ supervisorRunMtimes.set(key, currentMtime);
171
182
  if (refreshed.status === "running") {
172
- await scheduleRun(cwd, runId, undefined, options);
183
+ const unchanged = previousMtime !== undefined &&
184
+ currentMtime !== undefined &&
185
+ currentMtime <= previousMtime;
186
+ if (!unchanged)
187
+ await scheduleRun(cwd, runId, undefined, options);
173
188
  return;
174
189
  }
175
190
  const existing = supervisorTimers.get(key);
176
191
  if (existing)
177
192
  clearInterval(existing);
178
193
  supervisorTimers.delete(key);
194
+ supervisorRunMtimes.delete(key);
179
195
  })().catch((error) => {
180
196
  void recordSupervisorError(cwd, runId, error);
181
197
  });
@@ -183,6 +199,16 @@ export function watchRun(cwd, runId, options = {}) {
183
199
  timer.unref?.();
184
200
  supervisorTimers.set(key, timer);
185
201
  }
202
+ async function readRunMtimeMs(cwd, runId) {
203
+ try {
204
+ return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
205
+ }
206
+ catch (error) {
207
+ if (error.code === "ENOENT")
208
+ return undefined;
209
+ throw error;
210
+ }
211
+ }
186
212
  export async function scheduleRun(cwd, runId, compiled, options = {}) {
187
213
  return withRunLease(cwd, runId, async () => {
188
214
  let run = await readRunRecord(cwd, runId);
@@ -629,6 +655,7 @@ async function executeDynamicControllerTask(cwd, run, compiledFlow, controllerIn
629
655
  sources,
630
656
  dynamic: compiledTask.dynamic,
631
657
  dynamicUi: options.dynamicUi,
658
+ availableModels: options.availableModels,
632
659
  });
633
660
  await assertDynamicGeneratedTasksSettled({
634
661
  cwd,
@@ -638,6 +665,7 @@ async function executeDynamicControllerTask(cwd, run, compiledFlow, controllerIn
638
665
  controllerTask: task,
639
666
  controllerCompiledTask: compiledTask,
640
667
  dynamic: compiledTask.dynamic,
668
+ availableModels: options.availableModels,
641
669
  });
642
670
  await recordActiveRuntime();
643
671
  const unrunBranchBlockers = await dynamicUnrunBranchBlockers(cwd, run.runId, task.specId);
@@ -1170,57 +1198,12 @@ function requiredDynamicString(value, field, api = "ctx.agent()") {
1170
1198
  }
1171
1199
  return value.trim();
1172
1200
  }
1173
- function optionalDynamicString(value, field) {
1174
- if (value === undefined)
1175
- return undefined;
1176
- return requiredDynamicString(value, field);
1177
- }
1178
- function optionalDynamicStringArray(value, field) {
1179
- if (value === undefined)
1180
- return undefined;
1181
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1182
- throw new Error(`ctx.agent() ${field} must be an array of strings`);
1183
- }
1184
- return value.map((item) => item.trim()).filter(Boolean);
1185
- }
1186
- function isPlainDynamicRecord(value) {
1187
- return typeof value === "object" && value !== null && !Array.isArray(value);
1188
- }
1189
- function optionalDynamicPositiveInteger(value, field) {
1190
- if (value === undefined)
1191
- return undefined;
1192
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1193
- throw new Error(`ctx.agent() ${field} must be a positive integer`);
1194
- }
1195
- return value;
1196
- }
1197
- function requiredDynamicOutputProfile(value, field, api) {
1198
- const profile = requiredDynamicString(value, field, api);
1199
- if (!isDynamicOutputProfile(profile)) {
1200
- throw new Error(`${api} ${field} has an unsupported output profile`);
1201
- }
1202
- return profile;
1203
- }
1204
- function requiredDynamicNonNegativeInteger(value, field, api) {
1205
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
1206
- throw new Error(`${api} ${field} must be a non-negative integer`);
1207
- }
1208
- return value;
1209
- }
1210
1201
  function requiredDynamicPositiveInteger(value, field, api) {
1211
1202
  if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1212
1203
  throw new Error(`${api} ${field} must be a positive integer`);
1213
1204
  }
1214
1205
  return value;
1215
1206
  }
1216
- function optionalDynamicStringField(value) {
1217
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
1218
- }
1219
- function optionalDynamicOutputProfile(value) {
1220
- if (value === undefined)
1221
- return undefined;
1222
- return requiredDynamicOutputProfile(value, "outputProfile", "ctx.agent()");
1223
- }
1224
1207
  async function currentDynamicBudgetRemaining(input) {
1225
1208
  const state = await readOrRebuildDynamicState(input.cwd, input.run.runId);
1226
1209
  const run = await readRunRecord(input.cwd, input.run.runId).catch(() => input.run);
@@ -1539,6 +1522,7 @@ async function repairMissingDynamicGeneratedTask(input, specId) {
1539
1522
  branchId: optionalEventString(event.payload.branchId),
1540
1523
  request,
1541
1524
  dynamic: input.dynamic,
1525
+ availableModels: input.availableModels,
1542
1526
  });
1543
1527
  assertDynamicGeneratedMetadataMatches(compiledTask, {
1544
1528
  controllerSpecId: input.controllerTask.specId,
@@ -1634,6 +1618,7 @@ async function runDynamicAgentRequest(input) {
1634
1618
  branchId: generationBranchId,
1635
1619
  request: generationRequest,
1636
1620
  dynamic: input.dynamic,
1621
+ availableModels: input.availableModels,
1637
1622
  });
1638
1623
  assertDynamicGeneratedMetadataMatches(compiledTask, {
1639
1624
  controllerSpecId: input.controllerTask.specId,