@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
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,
@@ -96,6 +96,7 @@ import {
96
96
  markDagDependentsSkipped,
97
97
  nextTaskRecordIndex,
98
98
  reconcileDynamicGeneratedRunRecords,
99
+ reconcileForeachGeneratedRunRecords,
99
100
  recoverStaleRunningDynamicControllers,
100
101
  replaceDependencyList,
101
102
  sourceStageIdsForFrom,
@@ -116,10 +117,6 @@ import {
116
117
  readSupportSources,
117
118
  writeArtifactGraphDynamicResult,
118
119
  } from "./artifact-graph-runtime.js";
119
- import {
120
- isDynamicOutputProfile,
121
- type DynamicOutputProfile,
122
- } from "./dynamic-profiles.js";
123
120
  import {
124
121
  DIRECT_DYNAMIC_RUNTIME_VERSION,
125
122
  ensureDirectDynamicRuntimeBundle,
@@ -128,7 +125,6 @@ import {
128
125
  type CompiledDynamicWorkflowTask,
129
126
  type CompiledTask,
130
127
  type CompiledWorkflow,
131
- type ThinkingLevel,
132
128
  WORKFLOW_RUN_TYPE,
133
129
  type WorkflowIndexRecord,
134
130
  type WorkflowRunRecord,
@@ -151,10 +147,14 @@ const DYNAMIC_CONTROLLER_ENGINE_CAPABILITIES = Object.freeze({
151
147
  const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE =
152
148
  "incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
153
149
  const supervisorTimers = new Map<string, ReturnType<typeof setInterval>>();
150
+ const supervisorRunMtimes = new Map<string, number>();
151
+ const supervisorErrorCounts = new Map<string, number>();
152
+ const MAX_SUPERVISOR_CONSECUTIVE_ERRORS = 3;
154
153
 
155
154
  export interface WorkflowRunOptions {
156
155
  task?: string;
157
- runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
156
+ runtimeOverrides?: WorkflowRuntimeDefaults;
157
+ runtimeDefaults?: WorkflowRuntimeDefaults;
158
158
  availableModels?: WorkflowModelInfo[];
159
159
  dynamicUi?: DynamicWorkflowUi;
160
160
  runId?: string;
@@ -163,6 +163,7 @@ export interface WorkflowRunOptions {
163
163
 
164
164
  interface WorkflowScheduleOptions {
165
165
  dynamicUi?: DynamicWorkflowUi;
166
+ availableModels?: WorkflowModelInfo[];
166
167
  }
167
168
 
168
169
  export async function runWorkflowSpec(
@@ -207,6 +208,7 @@ async function runLoadedWorkflowSpec(
207
208
  cwd,
208
209
  specPath,
209
210
  task: options.task,
211
+ runtimeOverrides: options.runtimeOverrides,
210
212
  runtimeDefaults: options.runtimeDefaults,
211
213
  availableModels: options.availableModels,
212
214
  });
@@ -222,12 +224,15 @@ async function runLoadedWorkflowSpec(
222
224
  await writeRunRecord(cwd, run);
223
225
  });
224
226
 
227
+ const scheduleOptions = {
228
+ dynamicUi: options.dynamicUi,
229
+ availableModels: options.availableModels,
230
+ };
225
231
  const scheduled =
226
- (await scheduleRun(cwd, run.runId, compiled, {
227
- dynamicUi: options.dynamicUi,
228
- })) ?? (await readRunRecord(cwd, run.runId));
229
- if (scheduled.status === "running")
230
- watchRun(cwd, scheduled.runId, { dynamicUi: options.dynamicUi });
232
+ (await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
233
+ (await readRunRecord(cwd, run.runId));
234
+ if (shouldWatchRun(scheduled))
235
+ watchRun(cwd, scheduled.runId, scheduleOptions);
231
236
  return scheduled;
232
237
  }
233
238
 
@@ -243,6 +248,22 @@ export async function refreshRun(
243
248
  return refreshed ?? current;
244
249
  }
245
250
 
251
+ function hasActiveSchedulerWork(
252
+ run: Pick<WorkflowRunRecord, "status" | "taskSummary">,
253
+ ): boolean {
254
+ return (
255
+ run.status === "running" ||
256
+ run.taskSummary.running > 0 ||
257
+ run.taskSummary.pending > 0
258
+ );
259
+ }
260
+
261
+ function shouldWatchRun(
262
+ run: Pick<WorkflowRunRecord, "status" | "taskSummary">,
263
+ ): boolean {
264
+ return hasActiveSchedulerWork(run);
265
+ }
266
+
246
267
  export async function waitForRun(
247
268
  cwd: string,
248
269
  runIdOrPrefix: string,
@@ -253,7 +274,7 @@ export async function waitForRun(
253
274
  const deadline = Date.now() + timeout;
254
275
  let run = await refreshRun(cwd, runIdOrPrefix);
255
276
 
256
- while (run.status === "running") {
277
+ while (hasActiveSchedulerWork(run)) {
257
278
  const beforeScheduleRemaining = deadline - Date.now();
258
279
  if (beforeScheduleRemaining <= 0)
259
280
  throw new Error(
@@ -263,7 +284,7 @@ export async function waitForRun(
263
284
  run = await refreshRun(cwd, run.runId);
264
285
  const remaining = deadline - Date.now();
265
286
  if (remaining <= 0) {
266
- if (run.status !== "running") return run;
287
+ if (!hasActiveSchedulerWork(run)) return run;
267
288
  throw new Error(
268
289
  `Flow run still running after ${timeout}ms: ${run.runId}`,
269
290
  );
@@ -280,6 +301,48 @@ export interface ResumeRunSummary {
280
301
  resetTaskIds: string[];
281
302
  }
282
303
 
304
+ export interface StopRunSummary {
305
+ run: WorkflowRunRecord;
306
+ interruptedTaskIds: string[];
307
+ }
308
+
309
+ export async function stopRun(
310
+ cwd: string,
311
+ runIdOrPrefix: string,
312
+ ): Promise<StopRunSummary> {
313
+ const current = await readRunRecord(cwd, runIdOrPrefix);
314
+ const stopped = await withRunLease(cwd, current.runId, async () => {
315
+ const run = await readRunRecord(cwd, current.runId);
316
+ if (isTerminalWorkflowStatus(run.status)) {
317
+ throw new Error(
318
+ `stop requires a non-terminal run; ${run.runId} is ${run.status}`,
319
+ );
320
+ }
321
+ await resolveWorkflowBackend(run)
322
+ .cleanupRun(cwd, run)
323
+ .catch(() => undefined);
324
+ const interruptedTaskIds: string[] = [];
325
+ for (const task of run.tasks) {
326
+ if (
327
+ setTaskTerminal(task, "interrupted", "workflow_stopped", {
328
+ exitCode: 130,
329
+ lastMessage: "Workflow stopped by user request",
330
+ })
331
+ ) {
332
+ interruptedTaskIds.push(task.taskId);
333
+ }
334
+ }
335
+ await writeRunRecord(cwd, run);
336
+ unwatchRun(cwd, run.runId);
337
+ return { run, interruptedTaskIds };
338
+ });
339
+ if (!stopped)
340
+ throw new Error(
341
+ `Could not acquire workflow run lease for ${current.runId}`,
342
+ );
343
+ return stopped;
344
+ }
345
+
283
346
  export async function resumeRun(
284
347
  cwd: string,
285
348
  runIdOrPrefix: string,
@@ -312,6 +375,9 @@ export async function resumeRun(
312
375
  const resetTaskIds: string[] = [];
313
376
  const updated = await withRunLease(cwd, current.runId, async () => {
314
377
  const run = await readRunRecord(cwd, current.runId);
378
+ await resolveWorkflowBackend(run)
379
+ .cleanupRun(cwd, run)
380
+ .catch(() => undefined);
315
381
  for (const task of run.tasks) {
316
382
  if (resetTaskForResume(task)) resetTaskIds.push(task.taskId);
317
383
  }
@@ -330,7 +396,7 @@ export async function resumeRun(
330
396
  const scheduled =
331
397
  (await scheduleRun(cwd, current.runId, undefined, options)) ??
332
398
  (await readRunRecord(cwd, current.runId));
333
- if (scheduled.status === "running") watchRun(cwd, scheduled.runId, options);
399
+ if (shouldWatchRun(scheduled)) watchRun(cwd, scheduled.runId, options);
334
400
  return { run: scheduled, resetTaskIds };
335
401
  }
336
402
 
@@ -341,7 +407,7 @@ export async function resumeSupervisors(
341
407
  try {
342
408
  const runs = await listRunRecords(cwd);
343
409
  for (const run of runs) {
344
- if (run.status === "running") {
410
+ if (hasActiveSchedulerWork(run)) {
345
411
  await scheduleRun(cwd, run.runId, undefined, options).catch((error) =>
346
412
  recordSupervisorError(cwd, run.runId, error),
347
413
  );
@@ -356,6 +422,15 @@ export async function resumeSupervisors(
356
422
  }
357
423
  }
358
424
 
425
+ function unwatchRun(cwd: string, runId: string): void {
426
+ const key = `${cwd}\0${runId}`;
427
+ const existing = supervisorTimers.get(key);
428
+ if (existing) clearInterval(existing);
429
+ supervisorTimers.delete(key);
430
+ supervisorRunMtimes.delete(key);
431
+ supervisorErrorCounts.delete(key);
432
+ }
433
+
359
434
  export function watchRun(
360
435
  cwd: string,
361
436
  runId: string,
@@ -366,17 +441,36 @@ export function watchRun(
366
441
 
367
442
  const timer = setInterval(() => {
368
443
  void (async () => {
444
+ const previousMtime = supervisorRunMtimes.get(key);
445
+ const beforeMtime = await readRunMtimeMs(cwd, runId);
369
446
  const refreshed = await refreshRun(cwd, runId);
370
- if (refreshed.status === "running") {
371
- await scheduleRun(cwd, runId, undefined, options);
447
+ const afterMtime = await readRunMtimeMs(cwd, runId);
448
+ const currentMtime = afterMtime ?? beforeMtime;
449
+ if (currentMtime !== undefined)
450
+ supervisorRunMtimes.set(key, currentMtime);
451
+ supervisorErrorCounts.delete(key);
452
+
453
+ if (hasActiveSchedulerWork(refreshed)) {
454
+ const unchanged =
455
+ previousMtime !== undefined &&
456
+ currentMtime !== undefined &&
457
+ currentMtime <= previousMtime;
458
+ if (!unchanged) await scheduleRun(cwd, runId, undefined, options);
372
459
  return;
373
460
  }
374
461
 
375
- const existing = supervisorTimers.get(key);
376
- if (existing) clearInterval(existing);
377
- supervisorTimers.delete(key);
462
+ unwatchRun(cwd, runId);
378
463
  })().catch((error) => {
379
- void recordSupervisorError(cwd, runId, error);
464
+ if (isMissingRunError(error)) {
465
+ unwatchRun(cwd, runId);
466
+ return;
467
+ }
468
+ const failures = (supervisorErrorCounts.get(key) ?? 0) + 1;
469
+ supervisorErrorCounts.set(key, failures);
470
+ void recordSupervisorError(cwd, runId, error).finally(() => {
471
+ if (failures >= MAX_SUPERVISOR_CONSECUTIVE_ERRORS)
472
+ unwatchRun(cwd, runId);
473
+ });
380
474
  });
381
475
  }, POLL_INTERVAL_MS);
382
476
 
@@ -384,6 +478,29 @@ export function watchRun(
384
478
  supervisorTimers.set(key, timer);
385
479
  }
386
480
 
481
+ async function readRunMtimeMs(
482
+ cwd: string,
483
+ runId: string,
484
+ ): Promise<number | undefined> {
485
+ try {
486
+ return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
487
+ } catch (error) {
488
+ if (isEnoentError(error)) return undefined;
489
+ throw error;
490
+ }
491
+ }
492
+
493
+ function isEnoentError(error: unknown): boolean {
494
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
495
+ }
496
+
497
+ function isMissingRunError(error: unknown): boolean {
498
+ return (
499
+ isEnoentError(error) ||
500
+ (error instanceof Error && /^Flow run not found: /.test(error.message))
501
+ );
502
+ }
503
+
387
504
  export async function scheduleRun(
388
505
  cwd: string,
389
506
  runId: string,
@@ -393,7 +510,12 @@ export async function scheduleRun(
393
510
  return withRunLease(cwd, runId, async () => {
394
511
  let run = await readRunRecord(cwd, runId);
395
512
  run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
396
- if (run.taskSummary.blocked > 0 || isTerminalWorkflowStatus(run.status))
513
+ if (isTerminalWorkflowStatus(run.status)) return run;
514
+ if (
515
+ run.taskSummary.blocked > 0 &&
516
+ run.taskSummary.pending === 0 &&
517
+ run.taskSummary.running === 0
518
+ )
397
519
  return run;
398
520
 
399
521
  const compiledFlow =
@@ -492,7 +614,7 @@ export function formatRun(
492
614
  async function reconcileActiveRuns(cwd: string): Promise<void> {
493
615
  const runs = await listRunRecords(cwd);
494
616
  for (const run of runs) {
495
- if (run.status === "running")
617
+ if (hasActiveSchedulerWork(run))
496
618
  await refreshRun(cwd, run.runId).catch((error) =>
497
619
  recordSupervisorError(cwd, run.runId, error),
498
620
  );
@@ -504,7 +626,7 @@ async function reconcileIndexedActiveRuns(
504
626
  index: WorkflowIndexRecord,
505
627
  ): Promise<void> {
506
628
  for (const run of index.runs) {
507
- if (run.status === "running")
629
+ if (hasActiveSchedulerWork(run))
508
630
  await refreshRun(cwd, run.runId).catch((error) =>
509
631
  recordSupervisorError(cwd, run.runId, error),
510
632
  );
@@ -543,6 +665,16 @@ async function scheduleDag(
543
665
  compiledFlow,
544
666
  );
545
667
  if (loopReconciled) return;
668
+ const foreachReconciled = reconcileForeachGeneratedRunRecords(
669
+ cwd,
670
+ run,
671
+ compiledFlow,
672
+ );
673
+ if (foreachReconciled) {
674
+ await writeJsonAtomic(compiledWorkflowPath(cwd, run.runId), compiledFlow);
675
+ await writeRunRecord(cwd, run);
676
+ return;
677
+ }
546
678
  const dynamicReconciled = reconcileDynamicGeneratedRunRecords(
547
679
  cwd,
548
680
  run,
@@ -628,7 +760,7 @@ async function scheduleDag(
628
760
  index,
629
761
  options,
630
762
  );
631
- if (launched) running += 1;
763
+ if (launched && run.tasks[index]?.status === "running") running += 1;
632
764
  }
633
765
  }
634
766
 
@@ -788,6 +920,17 @@ async function materializeForeachTask(
788
920
 
789
921
  const placeholderSpecId = template.id;
790
922
  const generatedSpecIds = generated.tasks.map((task) => task.id);
923
+ const hasDownstreamDependents = compiledFlow.tasks.some(
924
+ (task, taskIndex) =>
925
+ taskIndex !== index && (task.dependsOn ?? []).includes(placeholderSpecId),
926
+ );
927
+ if (generatedSpecIds.length === 0 && !hasDownstreamDependents) {
928
+ setTaskTerminal(templateRunTask, "completed", "foreach_empty", {
929
+ lastMessage: "foreach produced 0 item(s)",
930
+ });
931
+ await writeRunRecord(cwd, run);
932
+ return true;
933
+ }
791
934
  compiledFlow.tasks.splice(index, 1, ...generated.tasks);
792
935
  updateDownstreamDependencies(
793
936
  compiledFlow,
@@ -880,12 +1023,15 @@ async function launchPendingTaskAt(
880
1023
  return false;
881
1024
  }
882
1025
 
883
- let launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
884
- if (task.outputRetry) {
885
- launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
886
- }
887
-
1026
+ let launchTask: CompiledWorkflow["tasks"][number] | undefined;
1027
+ let prepareComplete = false;
888
1028
  try {
1029
+ launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
1030
+ if (task.outputRetry) {
1031
+ launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
1032
+ }
1033
+ prepareComplete = true;
1034
+
889
1035
  if (launchTask.kind === "support") {
890
1036
  return await executeSupportTask(cwd, run, task, launchTask);
891
1037
  }
@@ -913,10 +1059,11 @@ async function launchPendingTaskAt(
913
1059
  if (launch.kind === "fatal") throw new Error(launch.message);
914
1060
  return launch.kind === "launched";
915
1061
  } catch (error) {
916
- const statusDetail =
917
- launchTask.kind === "support"
1062
+ const statusDetail = !prepareComplete
1063
+ ? "prepare_failed"
1064
+ : launchTask?.kind === "support"
918
1065
  ? "support_failed"
919
- : launchTask.safety.requiresWorktree
1066
+ : launchTask?.safety.requiresWorktree
920
1067
  ? "worktree_failed"
921
1068
  : "launch_failed";
922
1069
  setTaskTerminal(task, "failed", statusDetail, {
@@ -1047,6 +1194,7 @@ async function executeDynamicControllerTask(
1047
1194
  sources,
1048
1195
  dynamic: compiledTask.dynamic,
1049
1196
  dynamicUi: options.dynamicUi,
1197
+ availableModels: options.availableModels,
1050
1198
  });
1051
1199
  await assertDynamicGeneratedTasksSettled({
1052
1200
  cwd,
@@ -1056,6 +1204,7 @@ async function executeDynamicControllerTask(
1056
1204
  controllerTask: task,
1057
1205
  controllerCompiledTask: compiledTask,
1058
1206
  dynamic: compiledTask.dynamic,
1207
+ availableModels: options.availableModels,
1059
1208
  });
1060
1209
  await recordActiveRuntime();
1061
1210
  const unrunBranchBlockers = await dynamicUnrunBranchBlockers(
@@ -1188,6 +1337,7 @@ async function runDynamicControllerWorker(input: {
1188
1337
  sources: Record<string, unknown>;
1189
1338
  dynamic: CompiledDynamicWorkflowTask;
1190
1339
  dynamicUi?: DynamicWorkflowUi;
1340
+ availableModels?: WorkflowModelInfo[];
1191
1341
  }): Promise<unknown> {
1192
1342
  const resolved = await resolveWorkflowHelperRef(
1193
1343
  input.dynamic.uses,
@@ -1810,65 +1960,6 @@ function requiredDynamicString(
1810
1960
  return value.trim();
1811
1961
  }
1812
1962
 
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
1963
  function requiredDynamicPositiveInteger(
1873
1964
  value: unknown,
1874
1965
  field: string,
@@ -1880,17 +1971,6 @@ function requiredDynamicPositiveInteger(
1880
1971
  return value;
1881
1972
  }
1882
1973
 
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
1974
  async function currentDynamicBudgetRemaining(input: {
1895
1975
  cwd: string;
1896
1976
  run: WorkflowRunRecord;
@@ -2263,6 +2343,7 @@ async function assertDynamicGeneratedTasksSettled(input: {
2263
2343
  controllerTask: WorkflowTaskRunRecord;
2264
2344
  controllerCompiledTask: CompiledTask;
2265
2345
  dynamic: CompiledDynamicWorkflowTask;
2346
+ availableModels?: WorkflowModelInfo[];
2266
2347
  }): Promise<void> {
2267
2348
  const state = await readOrRebuildDynamicState(input.cwd, input.run.runId);
2268
2349
  const generatedTaskIds =
@@ -2289,6 +2370,7 @@ async function repairMissingDynamicGeneratedTask(
2289
2370
  controllerTask: WorkflowTaskRunRecord;
2290
2371
  controllerCompiledTask: CompiledTask;
2291
2372
  dynamic: CompiledDynamicWorkflowTask;
2373
+ availableModels?: WorkflowModelInfo[];
2292
2374
  },
2293
2375
  specId: string,
2294
2376
  ): Promise<WorkflowTaskRunRecord> {
@@ -2325,6 +2407,7 @@ async function repairMissingDynamicGeneratedTask(
2325
2407
  branchId: optionalEventString(event.payload.branchId),
2326
2408
  request,
2327
2409
  dynamic: input.dynamic,
2410
+ availableModels: input.availableModels,
2328
2411
  });
2329
2412
  assertDynamicGeneratedMetadataMatches(compiledTask, {
2330
2413
  controllerSpecId: input.controllerTask.specId,
@@ -2374,6 +2457,7 @@ async function runDynamicAgentRequest(input: {
2374
2457
  request: unknown;
2375
2458
  generatedTaskIds: string[];
2376
2459
  isSettled?: () => boolean;
2460
+ availableModels?: WorkflowModelInfo[];
2377
2461
  }): Promise<unknown> {
2378
2462
  await assertDynamicRuntimeBudgetAvailable({
2379
2463
  cwd: input.cwd,
@@ -2459,6 +2543,7 @@ async function runDynamicAgentRequest(input: {
2459
2543
  branchId: generationBranchId,
2460
2544
  request: generationRequest,
2461
2545
  dynamic: input.dynamic,
2546
+ availableModels: input.availableModels,
2462
2547
  });
2463
2548
  assertDynamicGeneratedMetadataMatches(compiledTask, {
2464
2549
  controllerSpecId: input.controllerTask.specId,