pi-workflow-engine 0.11.0 → 0.12.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 (73) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +255 -97
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
  6. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
  9. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
  11. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
  12. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
  13. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  14. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  15. package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
  17. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  18. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
  19. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
  20. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
  21. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
  22. package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
  23. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  24. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
  25. package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
  26. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  27. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  28. package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
  29. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  30. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  31. package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
  32. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  33. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  34. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  35. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +74 -38
  36. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
  37. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
  38. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
  39. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
  40. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
  41. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
  42. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  43. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  44. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  45. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  46. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
  47. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
  48. package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
  49. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  50. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  51. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  52. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +2 -1
  53. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  54. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  55. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  56. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
  57. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
  58. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  59. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  60. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  61. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  62. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  63. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  64. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
  65. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
  66. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
  67. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
  68. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
  69. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
  70. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  71. package/README.md +72 -141
  72. package/USAGE.md +211 -17
  73. package/package.json +1 -1
@@ -0,0 +1,310 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { Api, Model } from "@earendil-works/pi-ai";
5
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
6
+ import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
7
+ import { isMissingPathError } from "./filesystem-error.ts";
8
+ import { unknownErrorMessage } from "./unknown-error.ts";
9
+
10
+ export const WORKFLOW_MODEL_PROFILE_NAMES = ["small", "medium", "big"] as const;
11
+ export type WorkflowModelProfileName = (typeof WORKFLOW_MODEL_PROFILE_NAMES)[number];
12
+
13
+ export interface WorkflowModelProfileConfig {
14
+ readonly model: string;
15
+ readonly thinkingLevel?: ThinkingLevel;
16
+ }
17
+
18
+ export interface WorkflowModelProfileFile {
19
+ readonly profiles: Partial<Record<WorkflowModelProfileName, WorkflowModelProfileConfig>>;
20
+ }
21
+
22
+ export interface WorkflowModelProfilePaths {
23
+ readonly user: string;
24
+ readonly project: string;
25
+ }
26
+
27
+ interface ResolvedWorkflowModelProfileBase {
28
+ readonly name: WorkflowModelProfileName;
29
+ readonly model: Model<Api> | undefined;
30
+ readonly thinkingLevel: ThinkingLevel | undefined;
31
+ }
32
+
33
+ export type ResolvedWorkflowModelProfile = ResolvedWorkflowModelProfileBase & (
34
+ | { readonly source: "host" }
35
+ | { readonly source: "project" | "user"; readonly configPath: string }
36
+ );
37
+
38
+ export type ResolvedWorkflowModelProfiles = Readonly<Record<WorkflowModelProfileName, ResolvedWorkflowModelProfile>>;
39
+
40
+ export interface WorkflowModelRouteRequest {
41
+ readonly model?: string;
42
+ readonly thinkingLevel?: ThinkingLevel;
43
+ readonly profile?: WorkflowModelProfileName;
44
+ }
45
+
46
+ export class WorkflowModelProfileConfigError extends Error {
47
+ override readonly name = "WorkflowModelProfileConfigError";
48
+ readonly code = "WORKFLOW_MODEL_PROFILE_CONFIG";
49
+
50
+ constructor(
51
+ message: string,
52
+ readonly configPath: string,
53
+ readonly profile?: WorkflowModelProfileName,
54
+ ) {
55
+ super(message);
56
+ }
57
+ }
58
+
59
+ export const WORKFLOW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
60
+ const THINKING_LEVELS = new Set<ThinkingLevel>(WORKFLOW_THINKING_LEVELS);
61
+ const HOST_FALLBACK_THINKING: Record<WorkflowModelProfileName, ThinkingLevel> = {
62
+ small: "low",
63
+ medium: "medium",
64
+ big: "high",
65
+ };
66
+ const CONFIG_FILE_NAME = "workflow-models.json";
67
+
68
+ export function workflowModelProfilePaths(
69
+ cwd: string,
70
+ agentDir: string = getAgentDir(),
71
+ ): WorkflowModelProfilePaths {
72
+ return {
73
+ user: join(agentDir, CONFIG_FILE_NAME),
74
+ project: join(cwd, ".pi", CONFIG_FILE_NAME),
75
+ };
76
+ }
77
+
78
+ export function resolveWorkflowModelProfiles(input: {
79
+ readonly cwd: string;
80
+ readonly modelRegistry: Pick<ModelRegistry, "find">;
81
+ readonly hostModel: Model<Api> | undefined;
82
+ readonly paths?: WorkflowModelProfilePaths;
83
+ }): ResolvedWorkflowModelProfiles {
84
+ const paths = input.paths ?? workflowModelProfilePaths(input.cwd);
85
+ const user = readWorkflowModelProfileFile(paths.user);
86
+ const project = readWorkflowModelProfileFile(paths.project);
87
+ const resolved = { ...hostWorkflowModelProfiles(input.hostModel) };
88
+
89
+ for (const name of WORKFLOW_MODEL_PROFILE_NAMES) {
90
+ const projectProfile = project.profiles[name];
91
+ const configured = projectProfile ?? user.profiles[name];
92
+ if (!configured) continue;
93
+
94
+ const source = projectProfile ? "project" : "user";
95
+ const configPath = paths[source];
96
+ resolved[name] = {
97
+ name,
98
+ model: resolveConfiguredModel(name, configured.model, input.modelRegistry, configPath),
99
+ thinkingLevel: configured.thinkingLevel,
100
+ source,
101
+ configPath,
102
+ };
103
+ }
104
+
105
+ return resolved;
106
+ }
107
+
108
+ export function hostWorkflowModelProfiles(
109
+ hostModel: Model<Api> | undefined,
110
+ ): ResolvedWorkflowModelProfiles {
111
+ return {
112
+ small: { name: "small", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.small, source: "host" },
113
+ medium: { name: "medium", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.medium, source: "host" },
114
+ big: { name: "big", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.big, source: "host" },
115
+ };
116
+ }
117
+
118
+ export function resolveAgentModelProfile(
119
+ input: {
120
+ readonly request: WorkflowModelRouteRequest;
121
+ readonly profiles: ResolvedWorkflowModelProfiles;
122
+ readonly resolveExplicitModel: (modelRef: string) => Model<Api> | undefined;
123
+ readonly hostModel: Model<Api> | undefined;
124
+ },
125
+ ): { readonly model: Model<Api> | undefined; readonly thinkingLevel: ThinkingLevel | undefined } {
126
+ const profile = input.request.profile === undefined
127
+ ? undefined
128
+ : input.profiles[assertWorkflowModelProfileName(input.request.profile)];
129
+ return {
130
+ model: input.request.model === undefined
131
+ ? (profile?.model ?? input.hostModel)
132
+ : input.resolveExplicitModel(input.request.model),
133
+ thinkingLevel: input.request.thinkingLevel ?? profile?.thinkingLevel,
134
+ };
135
+ }
136
+
137
+ export function readWorkflowModelProfileFile(configPath: string): WorkflowModelProfileFile {
138
+ let contents: string;
139
+ try {
140
+ contents = readFileSync(configPath, "utf8");
141
+ } catch (error) {
142
+ if (isMissingPathError(error)) return { profiles: {} };
143
+ throw new WorkflowModelProfileConfigError(
144
+ `Could not read workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}. Fix the file permissions or remove the file.`,
145
+ configPath,
146
+ );
147
+ }
148
+ let parsed: unknown;
149
+ try {
150
+ parsed = JSON.parse(contents);
151
+ } catch (error) {
152
+ throw new WorkflowModelProfileConfigError(
153
+ `Invalid JSON in workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}. Fix or remove the file.`,
154
+ configPath,
155
+ );
156
+ }
157
+ return parseWorkflowModelProfileFile(parsed, configPath);
158
+ }
159
+
160
+ export function setWorkflowModelProfile(input: {
161
+ readonly configPath: string;
162
+ readonly name: WorkflowModelProfileName;
163
+ readonly profile: WorkflowModelProfileConfig;
164
+ readonly modelRegistry: Pick<ModelRegistry, "find">;
165
+ }): void {
166
+ const current = readWorkflowModelProfileFile(input.configPath);
167
+ const next = { ...current.profiles, [input.name]: input.profile };
168
+ for (const name of WORKFLOW_MODEL_PROFILE_NAMES) {
169
+ const profile = next[name];
170
+ if (profile) resolveConfiguredModel(name, profile.model, input.modelRegistry, input.configPath);
171
+ }
172
+ writeWorkflowModelProfileFile(input.configPath, { profiles: next });
173
+ }
174
+
175
+ export function clearWorkflowModelProfile(configPath: string, name: WorkflowModelProfileName): void {
176
+ const current = readWorkflowModelProfileFile(configPath);
177
+ if (!current.profiles[name]) return;
178
+ const profiles = { ...current.profiles };
179
+ delete profiles[name];
180
+ writeWorkflowModelProfileFile(configPath, { profiles });
181
+ }
182
+
183
+ export function isWorkflowModelProfileName(value: string): value is WorkflowModelProfileName {
184
+ return WORKFLOW_MODEL_PROFILE_NAMES.includes(value as WorkflowModelProfileName);
185
+ }
186
+
187
+ export function isWorkflowThinkingLevel(value: string): value is ThinkingLevel {
188
+ return THINKING_LEVELS.has(value as ThinkingLevel);
189
+ }
190
+
191
+ function parseWorkflowModelProfileFile(value: unknown, configPath: string): WorkflowModelProfileFile {
192
+ if (!isRecord(value)) throw invalidConfig(configPath, "the root must be an object");
193
+ assertOnlyKeys(value, ["profiles"], configPath);
194
+ if (!isRecord(value.profiles)) throw invalidConfig(configPath, '"profiles" must be an object');
195
+
196
+ const profiles: Partial<Record<WorkflowModelProfileName, WorkflowModelProfileConfig>> = {};
197
+ for (const [name, profile] of Object.entries(value.profiles)) {
198
+ if (!isWorkflowModelProfileName(name)) {
199
+ throw invalidConfig(configPath, `unknown profile "${name}"; expected small, medium, or big`);
200
+ }
201
+ if (!isRecord(profile)) throw invalidConfig(configPath, `profile "${name}" must be an object`, name);
202
+ assertOnlyKeys(profile, ["model", "thinkingLevel"], configPath, name);
203
+ if (typeof profile.model !== "string") {
204
+ throw invalidConfig(configPath, `profile "${name}" requires a provider-qualified "model" string`, name);
205
+ }
206
+ assertThinkingLevel(profile.thinkingLevel, configPath, name);
207
+ profiles[name] = {
208
+ model: profile.model,
209
+ ...(profile.thinkingLevel === undefined ? {} : { thinkingLevel: profile.thinkingLevel }),
210
+ };
211
+ }
212
+ return { profiles };
213
+ }
214
+
215
+ function resolveConfiguredModel(
216
+ name: WorkflowModelProfileName,
217
+ modelRef: string,
218
+ modelRegistry: Pick<ModelRegistry, "find">,
219
+ configPath: string,
220
+ ): Model<Api> {
221
+ const slash = modelRef.indexOf("/");
222
+ if (modelRef.trim() !== modelRef || slash <= 0 || slash === modelRef.length - 1) {
223
+ throw invalidConfig(
224
+ configPath,
225
+ `profile "${name}" model must be an exact provider/model identity; received "${modelRef}"`,
226
+ name,
227
+ );
228
+ }
229
+ const provider = modelRef.slice(0, slash);
230
+ const modelId = modelRef.slice(slash + 1);
231
+ if (/\s/.test(provider) || modelId.startsWith("/") || modelId.endsWith("/") || /\s/.test(modelId)) {
232
+ throw invalidConfig(
233
+ configPath,
234
+ `profile "${name}" model must be an exact provider/model identity; received "${modelRef}"`,
235
+ name,
236
+ );
237
+ }
238
+ const model = modelRegistry.find(provider, modelId);
239
+ if (!model) {
240
+ throw invalidConfig(
241
+ configPath,
242
+ `profile "${name}" references unavailable model "${modelRef}"; choose an exact model shown by /model and update it with /workflow:models set ${name} provider/model`,
243
+ name,
244
+ );
245
+ }
246
+ return model;
247
+ }
248
+
249
+ function assertWorkflowModelProfileName(value: string): WorkflowModelProfileName {
250
+ if (isWorkflowModelProfileName(value)) return value;
251
+ throw new Error(`Unknown workflow model profile "${value}"; expected small, medium, or big.`);
252
+ }
253
+
254
+ function assertThinkingLevel(
255
+ value: unknown,
256
+ configPath: string,
257
+ profile: WorkflowModelProfileName,
258
+ ): asserts value is ThinkingLevel | undefined {
259
+ if (value === undefined || (typeof value === "string" && isWorkflowThinkingLevel(value))) return;
260
+ throw invalidConfig(
261
+ configPath,
262
+ `profile "${profile}" thinkingLevel must be off, minimal, low, medium, high, xhigh, or max`,
263
+ profile,
264
+ );
265
+ }
266
+
267
+ function assertOnlyKeys(
268
+ value: Record<string, unknown>,
269
+ allowed: readonly string[],
270
+ configPath: string,
271
+ profile?: WorkflowModelProfileName,
272
+ ): void {
273
+ const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
274
+ if (unexpected) throw invalidConfig(configPath, `unexpected key "${unexpected}"`, profile);
275
+ }
276
+
277
+ function writeWorkflowModelProfileFile(configPath: string, config: WorkflowModelProfileFile): void {
278
+ const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
279
+ try {
280
+ mkdirSync(dirname(configPath), { recursive: true });
281
+ writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
282
+ renameSync(temporaryPath, configPath);
283
+ } catch (error) {
284
+ try {
285
+ unlinkSync(temporaryPath);
286
+ } catch {
287
+ // Best effort: preserve the original write error.
288
+ }
289
+ throw new WorkflowModelProfileConfigError(
290
+ `Could not write workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}.`,
291
+ configPath,
292
+ );
293
+ }
294
+ }
295
+
296
+ function invalidConfig(
297
+ configPath: string,
298
+ reason: string,
299
+ profile?: WorkflowModelProfileName,
300
+ ): WorkflowModelProfileConfigError {
301
+ return new WorkflowModelProfileConfigError(
302
+ `Invalid workflow model profile config at ${configPath}: ${reason}.`,
303
+ configPath,
304
+ profile,
305
+ );
306
+ }
307
+
308
+ function isRecord(value: unknown): value is Record<string, unknown> {
309
+ return typeof value === "object" && value !== null && !Array.isArray(value);
310
+ }
@@ -3,14 +3,48 @@ import type { WorkflowRunOptions } from "./types.ts";
3
3
 
4
4
  export const WORKFLOW_BUDGET_MIN = 1;
5
5
  export const WORKFLOW_BUDGET_MAX = 1_000_000_000;
6
+ export const WORKFLOW_MAX_AGENTS_MIN = 1;
7
+ export const WORKFLOW_MAX_AGENTS_MAX = 10_000;
8
+ export const DEFAULT_WORKFLOW_MAX_AGENTS = 64;
9
+ export const WORKFLOW_AGENT_TIMEOUT_MIN_MS = 1_000;
10
+ export const WORKFLOW_AGENT_TIMEOUT_MAX_MS = 86_400_000;
11
+ export const DEFAULT_WORKFLOW_AGENT_TIMEOUT_MS = 1_800_000;
12
+ export const WORKFLOW_AGENT_RETRIES_MIN = 0;
13
+ export const WORKFLOW_AGENT_RETRIES_MAX = 10;
14
+ export const DEFAULT_WORKFLOW_AGENT_RETRIES = 0;
15
+ export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN = 1;
16
+ export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX = 10;
17
+ export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS = 3;
18
+ export const WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS = 5_000;
19
+ export const WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS = 86_400_000;
20
+ export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS = 21_600_000;
6
21
 
7
22
  export type ResolvedWorkflowRunOptions = Omit<
8
23
  WorkflowRunOptions,
9
- "perf" | "concurrency" | "parallelSubmissionLimit" | "budget"
24
+ | "perf"
25
+ | "concurrency"
26
+ | "parallelSubmissionLimit"
27
+ | "maxAgents"
28
+ | "agentTimeoutMs"
29
+ | "agentRetries"
30
+ | "autoResumeOnUsageLimit"
31
+ | "usageLimitMaxAttempts"
32
+ | "usageLimitMaxDelayMs"
33
+ | "usageLimitAttempt"
34
+ | "resumeEditedWorkflow"
35
+ | "budget"
10
36
  > & {
11
37
  readonly perf: boolean;
12
38
  readonly concurrency: number;
13
39
  readonly parallelSubmissionLimit: number | null;
40
+ readonly maxAgents: number;
41
+ readonly agentTimeoutMs: number;
42
+ readonly agentRetries: number;
43
+ readonly autoResumeOnUsageLimit: boolean;
44
+ readonly usageLimitMaxAttempts: number;
45
+ readonly usageLimitMaxDelayMs: number;
46
+ readonly usageLimitAttempt: number;
47
+ readonly resumeEditedWorkflow: boolean;
14
48
  readonly budget: number | null;
15
49
  };
16
50
 
@@ -28,16 +62,63 @@ export function resolveWorkflowRunOptions(
28
62
  1,
29
63
  10_000,
30
64
  );
65
+ const maxAgents = clampInteger(
66
+ input.maxAgents ?? parseWorkflowIntegerString(env.PI_WORKFLOW_MAX_AGENTS),
67
+ WORKFLOW_MAX_AGENTS_MIN,
68
+ WORKFLOW_MAX_AGENTS_MAX,
69
+ DEFAULT_WORKFLOW_MAX_AGENTS,
70
+ );
71
+ const agentTimeoutMs = clampInteger(
72
+ input.agentTimeoutMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_TIMEOUT_MS),
73
+ WORKFLOW_AGENT_TIMEOUT_MIN_MS,
74
+ WORKFLOW_AGENT_TIMEOUT_MAX_MS,
75
+ DEFAULT_WORKFLOW_AGENT_TIMEOUT_MS,
76
+ );
77
+ const agentRetries = clampInteger(
78
+ input.agentRetries ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_RETRIES),
79
+ WORKFLOW_AGENT_RETRIES_MIN,
80
+ WORKFLOW_AGENT_RETRIES_MAX,
81
+ DEFAULT_WORKFLOW_AGENT_RETRIES,
82
+ );
31
83
  const budget = resolveBudget(input.budget, env.PI_WORKFLOW_BUDGET);
84
+ const usageLimitMaxAttempts = clampInteger(
85
+ input.usageLimitMaxAttempts ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS),
86
+ WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
87
+ WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
88
+ DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS,
89
+ );
90
+ const usageLimitMaxDelayMs = clampInteger(
91
+ input.usageLimitMaxDelayMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS),
92
+ WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
93
+ WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
94
+ DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS,
95
+ );
32
96
  return {
33
97
  ...input,
34
98
  perf: input.perf ?? env.PI_WORKFLOW_PERF === "1",
35
99
  concurrency,
36
100
  parallelSubmissionLimit: parallelSubmissionLimit ?? null,
101
+ maxAgents,
102
+ agentTimeoutMs,
103
+ agentRetries,
104
+ autoResumeOnUsageLimit: input.autoResumeOnUsageLimit ?? env.PI_WORKFLOW_USAGE_LIMIT_AUTO_RESUME === "1",
105
+ usageLimitMaxAttempts,
106
+ usageLimitMaxDelayMs,
107
+ usageLimitAttempt: clampInteger(
108
+ input.usageLimitAttempt,
109
+ 0,
110
+ WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
111
+ 0,
112
+ ),
113
+ resumeEditedWorkflow: input.resumeEditedWorkflow === true && hasResumeRunId(input.resumeFromRunId),
37
114
  budget: budget ?? null,
38
115
  };
39
116
  }
40
117
 
118
+ function hasResumeRunId(value: string | undefined): boolean {
119
+ return typeof value === "string" && value.trim().length > 0;
120
+ }
121
+
41
122
  function optionalClampedInteger(value: number | undefined, min: number, max: number): number | undefined {
42
123
  if (value === undefined || !Number.isFinite(value)) return undefined;
43
124
  return clampInteger(value, min, max, value);
@@ -54,6 +135,12 @@ function parseInteger(value: string | undefined): number | undefined {
54
135
  return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
55
136
  }
56
137
 
138
+ export function parseWorkflowIntegerString(value: string | undefined): number | undefined {
139
+ if (value === undefined || value.trim() === "") return undefined;
140
+ const parsed = Number(value);
141
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
142
+ }
143
+
57
144
  function resolveBudget(inputBudget: number | undefined, envBudget: string | undefined): number | undefined {
58
145
  if (inputBudget !== undefined) return normalizeExplicitBudget(inputBudget);
59
146
  return envBudget === undefined ? undefined : parseWorkflowBudgetString(envBudget);
@@ -0,0 +1,32 @@
1
+ const MINIMUM_PI_VERSION = "0.80.10";
2
+
3
+ const MINIMUM_PI_CORE = [0, 80, 10] as const;
4
+
5
+ /** Fail during extension registration instead of failing later in a workflow. */
6
+ export function assertSupportedPiVersion(version: string): void {
7
+ const core = parseVersionCore(version);
8
+ if (!core || compareVersionCore(core, MINIMUM_PI_CORE) < 0) {
9
+ throw new Error(
10
+ `pi-workflow-engine requires pi ${MINIMUM_PI_VERSION} or newer; detected ${JSON.stringify(version)}. Update pi before loading this extension.`,
11
+ );
12
+ }
13
+ }
14
+
15
+ function parseVersionCore(version: string): readonly [number, number, number] | undefined {
16
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
17
+ if (!match) return undefined;
18
+ const major = Number(match[1]);
19
+ const minor = Number(match[2]);
20
+ const patch = Number(match[3]);
21
+ if (![major, minor, patch].every(Number.isSafeInteger)) return undefined;
22
+ return [major, minor, patch];
23
+ }
24
+
25
+ function compareVersionCore(
26
+ left: readonly [number, number, number],
27
+ right: readonly [number, number, number],
28
+ ): number {
29
+ const [leftMajor, leftMinor, leftPatch] = left;
30
+ const [rightMajor, rightMinor, rightPatch] = right;
31
+ return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch;
32
+ }
@@ -0,0 +1,50 @@
1
+ import type { WorkflowUsageSnapshot } from "./usage.ts";
2
+
3
+ export type AgentRowStatus = "queued" | "running" | "done" | "failed";
4
+ export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
5
+
6
+ export interface AgentRowSnapshot {
7
+ readonly id: number;
8
+ readonly label: string;
9
+ readonly status: AgentRowStatus;
10
+ readonly startedAt?: number;
11
+ readonly doneAt?: number;
12
+ readonly toolUses: number;
13
+ readonly lastTool?: string;
14
+ readonly error?: string;
15
+ }
16
+
17
+ export interface PhaseSnapshot {
18
+ readonly title: string;
19
+ readonly agents: readonly AgentRowSnapshot[];
20
+ }
21
+
22
+ export interface WorkflowCounterSnapshot {
23
+ readonly key: string;
24
+ readonly label: string;
25
+ readonly value: number;
26
+ }
27
+
28
+ export interface WorkflowLaneItemSnapshot {
29
+ readonly lane: string;
30
+ readonly title: string;
31
+ readonly subtitle?: string;
32
+ readonly status: WorkflowLaneItemStatus;
33
+ readonly details?: string;
34
+ readonly createdAt: number;
35
+ }
36
+
37
+ export interface WorkflowProgressSnapshot {
38
+ readonly runId: string;
39
+ readonly title: string;
40
+ readonly startedAt: number;
41
+ readonly doneAt?: number;
42
+ readonly currentPhase: string;
43
+ readonly phases: readonly PhaseSnapshot[];
44
+ readonly counters: readonly WorkflowCounterSnapshot[];
45
+ readonly summary: readonly [string, string | number][];
46
+ readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
47
+ readonly laneOverflow: readonly [string, number][];
48
+ readonly logs: readonly string[];
49
+ readonly usage?: WorkflowUsageSnapshot;
50
+ }