@agwab/pi-workflow 0.2.0 → 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.
package/src/compiler.ts CHANGED
@@ -33,7 +33,10 @@ import {
33
33
  } from "./types.js";
34
34
  import {
35
35
  resolveWorkflowRuntime,
36
+ selectWorkflowRuntime,
36
37
  type WorkflowModelInfo,
38
+ type WorkflowRuntimeDefaults,
39
+ type WorkflowRuntimeResolutionInput,
37
40
  } from "./workflow-runtime.js";
38
41
 
39
42
  const DELEGATION_TOOLS = new Set([
@@ -549,7 +552,8 @@ export async function compileWorkflow(
549
552
  spec: ArtifactGraphWorkflowSpec,
550
553
  options: CompileOptions & {
551
554
  task?: string;
552
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
555
+ runtimeOverrides?: WorkflowRuntimeDefaults;
556
+ runtimeDefaults?: WorkflowRuntimeDefaults;
553
557
  },
554
558
  ): Promise<any> {
555
559
  const compilePlan = buildArtifactGraphCompilePlan(spec, options);
@@ -615,11 +619,25 @@ async function collectForeachPathWarnings(
615
619
  return warnings;
616
620
  }
617
621
 
622
+ function runtimeSettings(value: unknown): WorkflowRuntimeDefaults | undefined {
623
+ if (!isPlainRecord(value)) return undefined;
624
+ const model =
625
+ typeof value.model === "string" && value.model.trim()
626
+ ? value.model.trim()
627
+ : undefined;
628
+ const thinking =
629
+ typeof value.thinking === "string" && value.thinking.trim()
630
+ ? (value.thinking.trim() as ThinkingLevel)
631
+ : undefined;
632
+ return model || thinking ? { model, thinking } : undefined;
633
+ }
634
+
618
635
  async function compileArtifactGraphPlan(
619
636
  spec: any,
620
637
  options: CompileOptions & {
621
638
  task?: string;
622
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
639
+ runtimeOverrides?: WorkflowRuntimeDefaults;
640
+ runtimeDefaults?: WorkflowRuntimeDefaults;
623
641
  },
624
642
  ): Promise<any> {
625
643
  const stages = spec.stages;
@@ -665,9 +683,9 @@ async function compileArtifactGraphPlan(
665
683
  Object.keys(workflowInput).length > 0
666
684
  ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
667
685
  : "";
668
- const defaultModel = options.runtimeDefaults?.model ?? spec.defaults?.model;
669
- const defaultThinking =
670
- options.runtimeDefaults?.thinking ?? spec.defaults?.thinking;
686
+ const runtimeOverrides = options.runtimeOverrides;
687
+ const runtimeDefaults = options.runtimeDefaults;
688
+ const specRuntimeDefaults = runtimeSettings(spec.defaults);
671
689
  const tasks: any[] = [];
672
690
  const stageRecords: any[] = [];
673
691
  const issues: ValidationIssue[] = [];
@@ -719,6 +737,22 @@ async function compileArtifactGraphPlan(
719
737
  dynamicToolPath,
720
738
  );
721
739
  const dynamicToolSelection = filterToolSelection(rawDynamicToolSelection);
740
+ const requestedRuntime = selectWorkflowRuntime(
741
+ runtimeOverrides,
742
+ runtimeSettings(stage),
743
+ runtimeDefaults,
744
+ specRuntimeDefaults,
745
+ );
746
+ const resolvedDynamicRuntime = await resolveWorkflowRuntime(
747
+ requestedRuntime,
748
+ {
749
+ taskKey: key,
750
+ stageId: stage.id,
751
+ taskId,
752
+ agent: "dynamic",
753
+ },
754
+ { availableModels: options.availableModels },
755
+ );
722
756
  const dynamicTask = buildDynamicTask(
723
757
  stage,
724
758
  taskId,
@@ -729,24 +763,22 @@ async function compileArtifactGraphPlan(
729
763
  specDir,
730
764
  workflowInputText,
731
765
  options.task,
732
- defaultModel,
733
- defaultThinking,
734
- overrides,
735
- );
736
- const resolvedDynamicRuntime = await resolveWorkflowRuntime(
737
- { model: defaultModel, thinking: defaultThinking },
766
+ resolvedDynamicRuntime,
738
767
  {
739
- taskKey: key,
740
- stageId: stage.id,
741
- taskId,
742
- agent: "dynamic",
768
+ runtimeOverrides,
769
+ runtimeDefaults,
770
+ specRuntimeDefaults,
771
+ stageRuntime: runtimeSettings(stage),
743
772
  },
744
- { availableModels: options.availableModels },
773
+ overrides,
745
774
  );
746
775
  dynamicTask.runtime = {
747
776
  ...dynamicTask.runtime,
748
777
  ...resolvedDynamicRuntime,
749
778
  };
779
+ if (options.availableModels?.length) {
780
+ dynamicTask.dynamic.availableModels = options.availableModels;
781
+ }
750
782
  if (dynamicToolSelection.tools || dynamicToolSelection.toolProviders) {
751
783
  dynamicTask.runtime = {
752
784
  ...dynamicTask.runtime,
@@ -823,15 +855,12 @@ async function compileArtifactGraphPlan(
823
855
  validateToolSubset(toolSelection.tools, stageAgent, issues, toolPath);
824
856
  validateDelegationBoundary(toolSelection.tools, issues, toolPath);
825
857
  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
- };
858
+ const requestedRuntime = selectWorkflowRuntime(
859
+ runtimeOverrides,
860
+ runtimeSettings(stage),
861
+ runtimeDefaults,
862
+ specRuntimeDefaults,
863
+ );
835
864
  const resolvedRuntime = await resolveWorkflowRuntime(
836
865
  requestedRuntime,
837
866
  {
@@ -1208,12 +1237,34 @@ async function compileArtifactGraphPlan(
1208
1237
  tasks,
1209
1238
  warnings,
1210
1239
  budget: {
1211
- models: defaultModel ? [{ model: defaultModel }] : [],
1240
+ models: budgetModelRows(tasks),
1212
1241
  unratedModels: [],
1213
1242
  },
1214
1243
  };
1215
1244
  }
1216
1245
 
1246
+ function budgetModelRows(tasks: any[]): Array<{ model: string }> {
1247
+ const models = new Set<string>();
1248
+ for (const task of tasks) {
1249
+ if (typeof task?.runtime?.model === "string" && task.runtime.model.trim()) {
1250
+ models.add(task.runtime.model.trim());
1251
+ }
1252
+ const loop = task?.dynamic?.decisionLoop;
1253
+ if (!loop || typeof loop !== "object") continue;
1254
+ for (const profile of [
1255
+ loop.planner,
1256
+ loop.workerDefaults,
1257
+ loop.verifier,
1258
+ loop.synthesis,
1259
+ ]) {
1260
+ if (typeof profile?.model === "string" && profile.model.trim()) {
1261
+ models.add(profile.model.trim());
1262
+ }
1263
+ }
1264
+ }
1265
+ return [...models].sort().map((model) => ({ model }));
1266
+ }
1267
+
1217
1268
  function isSupportStage(stage: any): boolean {
1218
1269
  return stage?.support !== undefined && stage?.type === undefined;
1219
1270
  }
@@ -1301,8 +1352,13 @@ function buildDynamicTask(
1301
1352
  specDir: string,
1302
1353
  workflowInputText: string,
1303
1354
  runtimeTask: string | undefined,
1304
- defaultModel: string | undefined,
1305
- defaultThinking: ThinkingLevel | undefined,
1355
+ controllerRuntime: WorkflowRuntimeResolutionInput,
1356
+ runtimePriority: {
1357
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1358
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1359
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1360
+ stageRuntime?: WorkflowRuntimeDefaults;
1361
+ },
1306
1362
  overrides: Partial<CompiledTask> & Record<string, unknown>,
1307
1363
  ): any {
1308
1364
  const dynamic = stage.dynamic ?? {};
@@ -1365,8 +1421,7 @@ function buildDynamicTask(
1365
1421
  }
1366
1422
  const decisionLoop = compileDynamicDecisionLoop(
1367
1423
  dynamic.decisionLoop,
1368
- defaultModel,
1369
- defaultThinking,
1424
+ runtimePriority,
1370
1425
  );
1371
1426
 
1372
1427
  return {
@@ -1386,8 +1441,7 @@ function buildDynamicTask(
1386
1441
  explicitWorktreePolicy: false,
1387
1442
  runtime: {
1388
1443
  approvalMode: "non-interactive",
1389
- model: defaultModel,
1390
- thinking: defaultThinking,
1444
+ ...controllerRuntime,
1391
1445
  maxRuntimeMs:
1392
1446
  dynamic.budget?.maxRuntimeMs ?? DEFAULT_DYNAMIC_MAX_RUNTIME_MS,
1393
1447
  },
@@ -1431,6 +1485,9 @@ function buildDynamicTask(
1431
1485
  helpers,
1432
1486
  workflows,
1433
1487
  ...(decisionLoop ? { decisionLoop } : {}),
1488
+ ...(runtimePriority.runtimeOverrides
1489
+ ? { runtimeOverrides: runtimePriority.runtimeOverrides }
1490
+ : {}),
1434
1491
  },
1435
1492
  ...overrides,
1436
1493
  };
@@ -1438,8 +1495,12 @@ function buildDynamicTask(
1438
1495
 
1439
1496
  function compileDynamicDecisionLoop(
1440
1497
  value: unknown,
1441
- defaultModel?: string,
1442
- defaultThinking?: ThinkingLevel,
1498
+ runtimePriority: {
1499
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1500
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1501
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1502
+ stageRuntime?: WorkflowRuntimeDefaults;
1503
+ },
1443
1504
  ): any | undefined {
1444
1505
  if (!isPlainRecord(value)) return undefined;
1445
1506
  const allowedToolSelection = filterToolSelection(
@@ -1455,25 +1516,18 @@ function compileDynamicDecisionLoop(
1455
1516
  recordValue(value.stateIndex, "requiredFindingIds"),
1456
1517
  );
1457
1518
  return {
1458
- planner: compileDynamicDecisionLoopProfile(
1459
- value.planner,
1460
- defaultModel,
1461
- defaultThinking,
1462
- ),
1519
+ planner: compileDynamicDecisionLoopProfile(value.planner, runtimePriority),
1463
1520
  workerDefaults: compileDynamicDecisionLoopProfile(
1464
1521
  value.workerDefaults,
1465
- defaultModel,
1466
- defaultThinking,
1522
+ runtimePriority,
1467
1523
  ),
1468
1524
  verifier: compileDynamicDecisionLoopProfile(
1469
1525
  value.verifier,
1470
- defaultModel,
1471
- defaultThinking,
1526
+ runtimePriority,
1472
1527
  ),
1473
1528
  synthesis: compileDynamicDecisionLoopProfile(
1474
1529
  value.synthesis,
1475
- defaultModel,
1476
- defaultThinking,
1530
+ runtimePriority,
1477
1531
  ),
1478
1532
  allowedAgents: stringArray(value.allowedAgents),
1479
1533
  ...(allowedToolSelection.tools
@@ -1528,8 +1582,12 @@ function compileDynamicDecisionLoop(
1528
1582
 
1529
1583
  function compileDynamicDecisionLoopProfile(
1530
1584
  value: unknown,
1531
- defaultModel?: string,
1532
- defaultThinking?: ThinkingLevel,
1585
+ runtimePriority: {
1586
+ runtimeOverrides?: WorkflowRuntimeDefaults;
1587
+ runtimeDefaults?: WorkflowRuntimeDefaults;
1588
+ specRuntimeDefaults?: WorkflowRuntimeDefaults;
1589
+ stageRuntime?: WorkflowRuntimeDefaults;
1590
+ },
1533
1591
  ): any | undefined {
1534
1592
  if (!isPlainRecord(value)) return undefined;
1535
1593
  const toolSelection = filterToolSelection(
@@ -1538,20 +1596,18 @@ function compileDynamicDecisionLoopProfile(
1538
1596
  undefined,
1539
1597
  ),
1540
1598
  );
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;
1599
+ const runtime = selectWorkflowRuntime(
1600
+ runtimePriority.runtimeOverrides,
1601
+ runtimeSettings(value),
1602
+ runtimePriority.stageRuntime,
1603
+ runtimePriority.runtimeDefaults,
1604
+ runtimePriority.specRuntimeDefaults,
1605
+ );
1549
1606
  return {
1550
1607
  ...(typeof value.agent === "string" && value.agent.trim()
1551
1608
  ? { agent: value.agent.trim() }
1552
1609
  : {}),
1553
- ...(model ? { model } : {}),
1554
- ...(thinking ? { thinking } : {}),
1610
+ ...runtime,
1555
1611
  ...(toolSelection.tools ? { tools: toolSelection.tools } : {}),
1556
1612
  ...(toolSelection.toolProviders
1557
1613
  ? { toolProviders: toolSelection.toolProviders }
@@ -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,
package/src/engine.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, extname, join, relative, 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
 
@@ -23,7 +23,7 @@ import {
23
23
  toProjectPath,
24
24
  updateIndex,
25
25
  withRunLease,
26
- workflowRunDir,
26
+ workflowRunPath,
27
27
  writeJsonAtomic,
28
28
  writeRunRecord,
29
29
  writeCompiledRunArtifact,
@@ -40,6 +40,7 @@ import {
40
40
  import {
41
41
  readSimpleJsonPath,
42
42
  type WorkflowModelInfo,
43
+ type WorkflowRuntimeDefaults,
43
44
  } from "./workflow-runtime.js";
44
45
  import {
45
46
  dynamicRunDir,
@@ -74,7 +75,6 @@ import {
74
75
  isDynamicCompiledTaskPayload,
75
76
  normalizeDynamicAgentRequest,
76
77
  readDynamicGeneratedTaskResult,
77
- type DynamicAgentRequest,
78
78
  } from "./dynamic-generated-task-runtime.js";
79
79
  import {
80
80
  optionalEventString,
@@ -116,10 +116,6 @@ import {
116
116
  readSupportSources,
117
117
  writeArtifactGraphDynamicResult,
118
118
  } from "./artifact-graph-runtime.js";
119
- import {
120
- isDynamicOutputProfile,
121
- type DynamicOutputProfile,
122
- } from "./dynamic-profiles.js";
123
119
  import {
124
120
  DIRECT_DYNAMIC_RUNTIME_VERSION,
125
121
  ensureDirectDynamicRuntimeBundle,
@@ -128,7 +124,6 @@ import {
128
124
  type CompiledDynamicWorkflowTask,
129
125
  type CompiledTask,
130
126
  type CompiledWorkflow,
131
- type ThinkingLevel,
132
127
  WORKFLOW_RUN_TYPE,
133
128
  type WorkflowIndexRecord,
134
129
  type WorkflowRunRecord,
@@ -151,10 +146,12 @@ const DYNAMIC_CONTROLLER_ENGINE_CAPABILITIES = Object.freeze({
151
146
  const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE =
152
147
  "incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
153
148
  const supervisorTimers = new Map<string, ReturnType<typeof setInterval>>();
149
+ const supervisorRunMtimes = new Map<string, number>();
154
150
 
155
151
  export interface WorkflowRunOptions {
156
152
  task?: string;
157
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
153
+ runtimeOverrides?: WorkflowRuntimeDefaults;
154
+ runtimeDefaults?: WorkflowRuntimeDefaults;
158
155
  availableModels?: WorkflowModelInfo[];
159
156
  dynamicUi?: DynamicWorkflowUi;
160
157
  runId?: string;
@@ -163,6 +160,7 @@ export interface WorkflowRunOptions {
163
160
 
164
161
  interface WorkflowScheduleOptions {
165
162
  dynamicUi?: DynamicWorkflowUi;
163
+ availableModels?: WorkflowModelInfo[];
166
164
  }
167
165
 
168
166
  export async function runWorkflowSpec(
@@ -207,6 +205,7 @@ async function runLoadedWorkflowSpec(
207
205
  cwd,
208
206
  specPath,
209
207
  task: options.task,
208
+ runtimeOverrides: options.runtimeOverrides,
210
209
  runtimeDefaults: options.runtimeDefaults,
211
210
  availableModels: options.availableModels,
212
211
  });
@@ -222,12 +221,15 @@ async function runLoadedWorkflowSpec(
222
221
  await writeRunRecord(cwd, run);
223
222
  });
224
223
 
224
+ const scheduleOptions = {
225
+ dynamicUi: options.dynamicUi,
226
+ availableModels: options.availableModels,
227
+ };
225
228
  const scheduled =
226
- (await scheduleRun(cwd, run.runId, compiled, {
227
- dynamicUi: options.dynamicUi,
228
- })) ?? (await readRunRecord(cwd, run.runId));
229
+ (await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
230
+ (await readRunRecord(cwd, run.runId));
229
231
  if (scheduled.status === "running")
230
- watchRun(cwd, scheduled.runId, { dynamicUi: options.dynamicUi });
232
+ watchRun(cwd, scheduled.runId, scheduleOptions);
231
233
  return scheduled;
232
234
  }
233
235
 
@@ -366,15 +368,27 @@ export function watchRun(
366
368
 
367
369
  const timer = setInterval(() => {
368
370
  void (async () => {
371
+ const previousMtime = supervisorRunMtimes.get(key);
372
+ const beforeMtime = await readRunMtimeMs(cwd, runId);
369
373
  const refreshed = await refreshRun(cwd, runId);
374
+ const afterMtime = await readRunMtimeMs(cwd, runId);
375
+ const currentMtime = afterMtime ?? beforeMtime;
376
+ if (currentMtime !== undefined)
377
+ supervisorRunMtimes.set(key, currentMtime);
378
+
370
379
  if (refreshed.status === "running") {
371
- await scheduleRun(cwd, runId, undefined, options);
380
+ const unchanged =
381
+ previousMtime !== undefined &&
382
+ currentMtime !== undefined &&
383
+ currentMtime <= previousMtime;
384
+ if (!unchanged) await scheduleRun(cwd, runId, undefined, options);
372
385
  return;
373
386
  }
374
387
 
375
388
  const existing = supervisorTimers.get(key);
376
389
  if (existing) clearInterval(existing);
377
390
  supervisorTimers.delete(key);
391
+ supervisorRunMtimes.delete(key);
378
392
  })().catch((error) => {
379
393
  void recordSupervisorError(cwd, runId, error);
380
394
  });
@@ -384,6 +398,18 @@ export function watchRun(
384
398
  supervisorTimers.set(key, timer);
385
399
  }
386
400
 
401
+ async function readRunMtimeMs(
402
+ cwd: string,
403
+ runId: string,
404
+ ): Promise<number | undefined> {
405
+ try {
406
+ return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
407
+ } catch (error) {
408
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
409
+ throw error;
410
+ }
411
+ }
412
+
387
413
  export async function scheduleRun(
388
414
  cwd: string,
389
415
  runId: string,
@@ -1047,6 +1073,7 @@ async function executeDynamicControllerTask(
1047
1073
  sources,
1048
1074
  dynamic: compiledTask.dynamic,
1049
1075
  dynamicUi: options.dynamicUi,
1076
+ availableModels: options.availableModels,
1050
1077
  });
1051
1078
  await assertDynamicGeneratedTasksSettled({
1052
1079
  cwd,
@@ -1056,6 +1083,7 @@ async function executeDynamicControllerTask(
1056
1083
  controllerTask: task,
1057
1084
  controllerCompiledTask: compiledTask,
1058
1085
  dynamic: compiledTask.dynamic,
1086
+ availableModels: options.availableModels,
1059
1087
  });
1060
1088
  await recordActiveRuntime();
1061
1089
  const unrunBranchBlockers = await dynamicUnrunBranchBlockers(
@@ -1188,6 +1216,7 @@ async function runDynamicControllerWorker(input: {
1188
1216
  sources: Record<string, unknown>;
1189
1217
  dynamic: CompiledDynamicWorkflowTask;
1190
1218
  dynamicUi?: DynamicWorkflowUi;
1219
+ availableModels?: WorkflowModelInfo[];
1191
1220
  }): Promise<unknown> {
1192
1221
  const resolved = await resolveWorkflowHelperRef(
1193
1222
  input.dynamic.uses,
@@ -1810,65 +1839,6 @@ function requiredDynamicString(
1810
1839
  return value.trim();
1811
1840
  }
1812
1841
 
1813
- function optionalDynamicString(
1814
- value: unknown,
1815
- field: string,
1816
- ): string | undefined {
1817
- if (value === undefined) return undefined;
1818
- return requiredDynamicString(value, field);
1819
- }
1820
-
1821
- function optionalDynamicStringArray(
1822
- value: unknown,
1823
- field: string,
1824
- ): string[] | undefined {
1825
- if (value === undefined) return undefined;
1826
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1827
- throw new Error(`ctx.agent() ${field} must be an array of strings`);
1828
- }
1829
- return value.map((item) => item.trim()).filter(Boolean);
1830
- }
1831
-
1832
- function isPlainDynamicRecord(
1833
- value: unknown,
1834
- ): value is Record<string, unknown> {
1835
- return typeof value === "object" && value !== null && !Array.isArray(value);
1836
- }
1837
-
1838
- function optionalDynamicPositiveInteger(
1839
- value: unknown,
1840
- field: string,
1841
- ): number | undefined {
1842
- if (value === undefined) return undefined;
1843
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1844
- throw new Error(`ctx.agent() ${field} must be a positive integer`);
1845
- }
1846
- return value;
1847
- }
1848
-
1849
- function requiredDynamicOutputProfile(
1850
- value: unknown,
1851
- field: string,
1852
- api: string,
1853
- ): DynamicOutputProfile {
1854
- const profile = requiredDynamicString(value, field, api);
1855
- if (!isDynamicOutputProfile(profile)) {
1856
- throw new Error(`${api} ${field} has an unsupported output profile`);
1857
- }
1858
- return profile;
1859
- }
1860
-
1861
- function requiredDynamicNonNegativeInteger(
1862
- value: unknown,
1863
- field: string,
1864
- api: string,
1865
- ): number {
1866
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
1867
- throw new Error(`${api} ${field} must be a non-negative integer`);
1868
- }
1869
- return value;
1870
- }
1871
-
1872
1842
  function requiredDynamicPositiveInteger(
1873
1843
  value: unknown,
1874
1844
  field: string,
@@ -1880,17 +1850,6 @@ function requiredDynamicPositiveInteger(
1880
1850
  return value;
1881
1851
  }
1882
1852
 
1883
- function optionalDynamicStringField(value: unknown): string | undefined {
1884
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
1885
- }
1886
-
1887
- function optionalDynamicOutputProfile(
1888
- value: unknown,
1889
- ): DynamicOutputProfile | undefined {
1890
- if (value === undefined) return undefined;
1891
- return requiredDynamicOutputProfile(value, "outputProfile", "ctx.agent()");
1892
- }
1893
-
1894
1853
  async function currentDynamicBudgetRemaining(input: {
1895
1854
  cwd: string;
1896
1855
  run: WorkflowRunRecord;
@@ -2263,6 +2222,7 @@ async function assertDynamicGeneratedTasksSettled(input: {
2263
2222
  controllerTask: WorkflowTaskRunRecord;
2264
2223
  controllerCompiledTask: CompiledTask;
2265
2224
  dynamic: CompiledDynamicWorkflowTask;
2225
+ availableModels?: WorkflowModelInfo[];
2266
2226
  }): Promise<void> {
2267
2227
  const state = await readOrRebuildDynamicState(input.cwd, input.run.runId);
2268
2228
  const generatedTaskIds =
@@ -2289,6 +2249,7 @@ async function repairMissingDynamicGeneratedTask(
2289
2249
  controllerTask: WorkflowTaskRunRecord;
2290
2250
  controllerCompiledTask: CompiledTask;
2291
2251
  dynamic: CompiledDynamicWorkflowTask;
2252
+ availableModels?: WorkflowModelInfo[];
2292
2253
  },
2293
2254
  specId: string,
2294
2255
  ): Promise<WorkflowTaskRunRecord> {
@@ -2325,6 +2286,7 @@ async function repairMissingDynamicGeneratedTask(
2325
2286
  branchId: optionalEventString(event.payload.branchId),
2326
2287
  request,
2327
2288
  dynamic: input.dynamic,
2289
+ availableModels: input.availableModels,
2328
2290
  });
2329
2291
  assertDynamicGeneratedMetadataMatches(compiledTask, {
2330
2292
  controllerSpecId: input.controllerTask.specId,
@@ -2374,6 +2336,7 @@ async function runDynamicAgentRequest(input: {
2374
2336
  request: unknown;
2375
2337
  generatedTaskIds: string[];
2376
2338
  isSettled?: () => boolean;
2339
+ availableModels?: WorkflowModelInfo[];
2377
2340
  }): Promise<unknown> {
2378
2341
  await assertDynamicRuntimeBudgetAvailable({
2379
2342
  cwd: input.cwd,
@@ -2459,6 +2422,7 @@ async function runDynamicAgentRequest(input: {
2459
2422
  branchId: generationBranchId,
2460
2423
  request: generationRequest,
2461
2424
  dynamic: input.dynamic,
2425
+ availableModels: input.availableModels,
2462
2426
  });
2463
2427
  assertDynamicGeneratedMetadataMatches(compiledTask, {
2464
2428
  controllerSpecId: input.controllerTask.specId,