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.
- package/.pi/extensions/pi-workflow-engine/index.ts +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +74 -38
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +211 -17
- package/package.json +1 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
3
|
+
import { VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
|
|
5
5
|
import { isAdvisoryReport } from "./src/advisory-schema.ts";
|
|
6
|
-
import type { WorkflowProgressSnapshot } from "./src/progress.ts";
|
|
6
|
+
import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
|
|
7
7
|
import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
|
|
8
8
|
import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
|
|
9
|
+
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
|
|
9
10
|
import type { PerfSink } from "./src/perf.ts";
|
|
10
11
|
import type { WorkflowUsageSnapshot } from "./src/usage.ts";
|
|
11
|
-
import { ADAPTIVE_WORKFLOW_GUIDANCE,
|
|
12
|
+
import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
|
|
13
|
+
import { sessionKey } from "./src/session-identity.ts";
|
|
12
14
|
import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
|
|
13
15
|
import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
|
|
14
16
|
import {
|
|
@@ -18,20 +20,36 @@ import {
|
|
|
18
20
|
} from "./src/ui/workflow-result-renderer.ts";
|
|
19
21
|
import {
|
|
20
22
|
parseWorkflowBudgetString,
|
|
23
|
+
parseWorkflowIntegerString,
|
|
21
24
|
resolveWorkflowRunOptions,
|
|
22
25
|
type ResolvedWorkflowRunOptions,
|
|
26
|
+
WORKFLOW_AGENT_TIMEOUT_MAX_MS,
|
|
27
|
+
WORKFLOW_AGENT_TIMEOUT_MIN_MS,
|
|
28
|
+
WORKFLOW_AGENT_RETRIES_MAX,
|
|
29
|
+
WORKFLOW_AGENT_RETRIES_MIN,
|
|
23
30
|
WORKFLOW_BUDGET_MAX,
|
|
24
31
|
WORKFLOW_BUDGET_MIN,
|
|
32
|
+
WORKFLOW_MAX_AGENTS_MAX,
|
|
33
|
+
WORKFLOW_MAX_AGENTS_MIN,
|
|
34
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
35
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
|
|
36
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
|
|
37
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
25
38
|
} from "./src/options.ts";
|
|
26
39
|
import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
|
|
40
|
+
import { registerWorkflowModelProfileCommand } from "./src/model-profile-command.ts";
|
|
41
|
+
import { BackgroundWorkflowCoordinator } from "./src/background-workflows.ts";
|
|
42
|
+
import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./src/background-workflow-tool.ts";
|
|
43
|
+
import { registerWorkflowRunCommand, WorkflowRunController } from "./src/workflow-run-controller.ts";
|
|
44
|
+
import { completeCurrentArgument, splitArgumentPrefix } from "./src/command-completions.ts";
|
|
45
|
+
import { assertSupportedPiVersion } from "./src/pi-compat.ts";
|
|
46
|
+
import { formatWorkflowInspection, workflowInspectionSnapshot } from "./src/ui/workflow-format.ts";
|
|
27
47
|
|
|
28
48
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
29
49
|
const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
30
50
|
|
|
31
51
|
function summarize(result: unknown): string {
|
|
32
|
-
if (
|
|
33
|
-
return (result as { summary: string }).summary;
|
|
34
|
-
}
|
|
52
|
+
if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
|
|
35
53
|
return typeof result === "string" ? result : "Workflow finished.";
|
|
36
54
|
}
|
|
37
55
|
|
|
@@ -117,77 +135,55 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
|
|
|
117
135
|
const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
|
|
118
136
|
const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
|
|
119
137
|
const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
|
|
120
|
-
const WORKFLOW_PICKER_MAX_VISIBLE = 8;
|
|
121
|
-
const WORKFLOW_PICKER_LAYOUT = { minPrimaryColumnWidth: 18, maxPrimaryColumnWidth: 22 };
|
|
122
|
-
|
|
123
|
-
class WorkflowPickerComponent implements Component {
|
|
124
|
-
private readonly list: SelectList;
|
|
125
|
-
|
|
126
|
-
constructor(
|
|
127
|
-
items: SelectItem[],
|
|
128
|
-
private readonly tui: TUI,
|
|
129
|
-
private readonly theme: Theme,
|
|
130
|
-
done: (value: string | undefined) => void,
|
|
131
|
-
) {
|
|
132
|
-
this.list = new SelectList(items, Math.min(WORKFLOW_PICKER_MAX_VISIBLE, items.length), selectListTheme(theme), WORKFLOW_PICKER_LAYOUT);
|
|
133
|
-
this.list.onSelect = (item) => done(item.value);
|
|
134
|
-
this.list.onCancel = () => done(undefined);
|
|
135
|
-
this.list.onSelectionChange = () => this.tui.requestRender();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
render(width: number): string[] {
|
|
139
|
-
const safeWidth = Math.max(20, width);
|
|
140
|
-
const rule = this.theme.fg("borderMuted", "-".repeat(safeWidth));
|
|
141
|
-
const lines = [
|
|
142
|
-
rule,
|
|
143
|
-
this.theme.fg("accent", this.theme.bold("Run workflow")),
|
|
144
|
-
"",
|
|
145
|
-
...this.list.render(safeWidth),
|
|
146
|
-
"",
|
|
147
|
-
this.theme.fg("muted", "up/down navigate enter select escape cancel"),
|
|
148
|
-
rule,
|
|
149
|
-
];
|
|
150
|
-
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
handleInput(data: string): void {
|
|
154
|
-
this.list.handleInput(data);
|
|
155
|
-
this.tui.requestRender();
|
|
156
|
-
}
|
|
157
138
|
|
|
158
|
-
|
|
159
|
-
|
|
139
|
+
const WORKFLOW_OPTION_COMPLETIONS = [
|
|
140
|
+
{ value: "--inspect", description: "Open the live workflow inspector" },
|
|
141
|
+
{ value: "--refresh", description: "Refresh dynamic workflow discovery" },
|
|
142
|
+
{ value: "--perf", description: "Collect workflow performance metrics" },
|
|
143
|
+
{ value: "--result-viewer", description: "Open supported result viewers" },
|
|
144
|
+
{ value: "--no-result-viewer", description: "Skip supported result viewers" },
|
|
145
|
+
{ value: "--resume-edited", description: "Allow resume after workflow source edits" },
|
|
146
|
+
{ value: "--concurrency=", description: "Set the global subagent concurrency cap" },
|
|
147
|
+
{ value: "--parallel-limit=", description: "Set the parallel submission limit" },
|
|
148
|
+
{ value: "--max-agents=", description: "Set the maximum admitted live agents" },
|
|
149
|
+
{ value: "--agent-timeout-ms=", description: "Set the timeout for each agent attempt" },
|
|
150
|
+
{ value: "--agent-retries=", description: "Set retries for each agent call" },
|
|
151
|
+
{ value: "--budget=", description: "Set the workflow output-token budget" },
|
|
152
|
+
{ value: "--resume=", description: "Resume from a retained workflow run ID" },
|
|
153
|
+
] as const;
|
|
154
|
+
|
|
155
|
+
async function workflowArgumentCompletions(argumentPrefix: string): Promise<AutocompleteItem[] | null> {
|
|
156
|
+
const parts = splitArgumentPrefix(argumentPrefix);
|
|
157
|
+
if (parts.completed.length === 0) {
|
|
158
|
+
const { discoverWorkflows } = await loadDiscovery();
|
|
159
|
+
const workflows = await discoverWorkflows(EXTENSION_DIR);
|
|
160
|
+
return completeCurrentArgument(
|
|
161
|
+
argumentPrefix,
|
|
162
|
+
[...workflows.values()].map((workflow) => ({
|
|
163
|
+
value: workflow.meta.name,
|
|
164
|
+
description: workflow.meta.description,
|
|
165
|
+
})),
|
|
166
|
+
);
|
|
160
167
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
function selectListTheme(theme: Theme): SelectListTheme {
|
|
164
|
-
return {
|
|
165
|
-
selectedPrefix: (text) => theme.fg("accent", text),
|
|
166
|
-
selectedText: (text) => theme.fg("accent", text),
|
|
167
|
-
description: (text) => theme.fg("muted", text),
|
|
168
|
-
scrollInfo: (text) => theme.fg("muted", text),
|
|
169
|
-
noMatch: (text) => theme.fg("muted", text),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function workflowPickerItems(workflows: ReadonlyMap<string, WorkflowModule>): SelectItem[] {
|
|
174
|
-
return [
|
|
175
|
-
{ value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL, description: AUTHOR_TEMP_WORKFLOW_DESCRIPTION },
|
|
176
|
-
...[...workflows.values()].map((mod) => ({ value: mod.meta.name, label: mod.meta.name, description: mod.meta.description })),
|
|
177
|
-
];
|
|
168
|
+
return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
|
|
178
169
|
}
|
|
179
170
|
|
|
180
171
|
async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
172
|
+
const choices = [
|
|
173
|
+
{
|
|
174
|
+
value: AUTHOR_TEMP_WORKFLOW_VALUE,
|
|
175
|
+
display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}`,
|
|
176
|
+
},
|
|
177
|
+
...[...workflows.values()].map((workflow) => ({
|
|
178
|
+
value: workflow.meta.name,
|
|
179
|
+
display: `${workflow.meta.name} — ${workflow.meta.description}`,
|
|
180
|
+
})),
|
|
181
|
+
];
|
|
182
|
+
const selected = await ctx.ui.select(
|
|
187
183
|
"Run workflow",
|
|
188
|
-
|
|
184
|
+
choices.map((choice) => choice.display),
|
|
189
185
|
);
|
|
190
|
-
return
|
|
186
|
+
return choices.find((choice) => choice.display === selected)?.value;
|
|
191
187
|
}
|
|
192
188
|
|
|
193
189
|
export interface LastWorkflowInspection {
|
|
@@ -217,20 +213,20 @@ export function getLastWorkflowInspection(): LastWorkflowInspection | undefined
|
|
|
217
213
|
return latestWorkflowInspection;
|
|
218
214
|
}
|
|
219
215
|
|
|
220
|
-
export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
|
|
221
|
-
return latestActiveWorkflowInspection;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
216
|
export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
|
|
217
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
218
|
+
ctx.ui.notify(formatWorkflowInspection(inspection), "info");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
225
221
|
await ctx.ui.custom<void>(
|
|
226
|
-
(tui, theme, _keybindings, done) => new WorkflowInspector(
|
|
227
|
-
|
|
222
|
+
(tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
|
|
223
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
228
224
|
);
|
|
229
225
|
}
|
|
230
226
|
|
|
231
227
|
function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
|
|
232
228
|
const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
|
|
233
|
-
const key =
|
|
229
|
+
const key = sessionKey(ctx);
|
|
234
230
|
const state = sessions.get(key) ?? {};
|
|
235
231
|
sessions.set(key, state);
|
|
236
232
|
workflowInspections.set(pi, sessions);
|
|
@@ -238,10 +234,6 @@ function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): Sessi
|
|
|
238
234
|
}
|
|
239
235
|
|
|
240
236
|
async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
241
|
-
if (!ctx.hasUI) {
|
|
242
|
-
ctx.ui.notify("Workflow inspector requires the TUI", "warning");
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
237
|
const state = workflowInspectionState(pi, ctx);
|
|
246
238
|
const inspection = state.active ?? state.last;
|
|
247
239
|
if (!inspection) {
|
|
@@ -251,11 +243,6 @@ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionCo
|
|
|
251
243
|
await openWorkflowInspector(ctx, inspection);
|
|
252
244
|
}
|
|
253
245
|
|
|
254
|
-
function snapshotGetter(inspection: LastWorkflowInspection | ActiveWorkflowInspection): () => WorkflowProgressSnapshot {
|
|
255
|
-
const snapshot = inspection.snapshot;
|
|
256
|
-
return typeof snapshot === "function" ? snapshot : () => snapshot;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
246
|
function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
|
|
260
247
|
return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
|
|
261
248
|
}
|
|
@@ -283,6 +270,10 @@ export function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
|
283
270
|
|
|
284
271
|
const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
|
|
285
272
|
const INVALID_RESUME_OPTION = "--resume requires a workflow run id";
|
|
273
|
+
const INVALID_MAX_AGENTS_OPTION = "--max-agents requires an integer";
|
|
274
|
+
const INVALID_AGENT_TIMEOUT_OPTION = "--agent-timeout-ms requires an integer";
|
|
275
|
+
const INVALID_AGENT_RETRIES_OPTION = "--agent-retries requires an integer";
|
|
276
|
+
const INVALID_EDITED_RESUME_OPTION = "--resume-edited requires --resume <run-id>";
|
|
286
277
|
|
|
287
278
|
function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
|
|
288
279
|
const tokens = input.split(/\s+/).filter(Boolean);
|
|
@@ -304,6 +295,10 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
304
295
|
options.perf = true;
|
|
305
296
|
continue;
|
|
306
297
|
}
|
|
298
|
+
if (token === "--resume-edited") {
|
|
299
|
+
options.resumeEditedWorkflow = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
307
302
|
if (token === "--result-viewer" || token === "--review-viewer") {
|
|
308
303
|
options.resultViewer = "open";
|
|
309
304
|
continue;
|
|
@@ -332,6 +327,54 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
332
327
|
if (next !== undefined) i++;
|
|
333
328
|
continue;
|
|
334
329
|
}
|
|
330
|
+
if (token.startsWith("--max-agents=")) {
|
|
331
|
+
const parsed = parseWorkflowIntegerString(token.slice("--max-agents=".length));
|
|
332
|
+
if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
|
|
333
|
+
else options.maxAgents = parsed;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (token === "--max-agents") {
|
|
337
|
+
const next = tokens[i + 1];
|
|
338
|
+
const parsed = parseWorkflowIntegerString(next);
|
|
339
|
+
if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
|
|
340
|
+
else {
|
|
341
|
+
options.maxAgents = parsed;
|
|
342
|
+
i++;
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (token.startsWith("--agent-timeout-ms=")) {
|
|
347
|
+
const parsed = parseWorkflowIntegerString(token.slice("--agent-timeout-ms=".length));
|
|
348
|
+
if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
|
|
349
|
+
else options.agentTimeoutMs = parsed;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (token === "--agent-timeout-ms") {
|
|
353
|
+
const next = tokens[i + 1];
|
|
354
|
+
const parsed = parseWorkflowIntegerString(next);
|
|
355
|
+
if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
|
|
356
|
+
else {
|
|
357
|
+
options.agentTimeoutMs = parsed;
|
|
358
|
+
i++;
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (token.startsWith("--agent-retries=")) {
|
|
363
|
+
const parsed = parseWorkflowIntegerString(token.slice("--agent-retries=".length));
|
|
364
|
+
if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
|
|
365
|
+
else options.agentRetries = parsed;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (token === "--agent-retries") {
|
|
369
|
+
const next = tokens[i + 1];
|
|
370
|
+
const parsed = parseWorkflowIntegerString(next);
|
|
371
|
+
if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
|
|
372
|
+
else {
|
|
373
|
+
options.agentRetries = parsed;
|
|
374
|
+
i++;
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
335
378
|
if (token.startsWith("--budget=")) {
|
|
336
379
|
const parsed = parseBudgetOption(token.slice("--budget=".length));
|
|
337
380
|
if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
|
|
@@ -367,6 +410,7 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
367
410
|
}
|
|
368
411
|
kept.push(token);
|
|
369
412
|
}
|
|
413
|
+
if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
|
|
370
414
|
return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
|
|
371
415
|
}
|
|
372
416
|
|
|
@@ -395,7 +439,7 @@ ${brief.trim()}
|
|
|
395
439
|
Use the workflow tool with a script argument, not a saved workflow name.
|
|
396
440
|
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
397
441
|
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
398
|
-
Set
|
|
442
|
+
Set profile to "small", "medium", or "big" on each agent() call; use explicit model/thinkingLevel only for an intentional override.
|
|
399
443
|
Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
|
|
400
444
|
If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
|
|
401
445
|
When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
|
|
@@ -408,6 +452,7 @@ export interface WorkflowToolRequestParams {
|
|
|
408
452
|
readonly name?: string;
|
|
409
453
|
readonly script?: string;
|
|
410
454
|
readonly resumeFromRunId?: string;
|
|
455
|
+
readonly background?: boolean;
|
|
411
456
|
}
|
|
412
457
|
|
|
413
458
|
export type WorkflowToolRequest =
|
|
@@ -570,10 +615,35 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
|
|
|
570
615
|
}
|
|
571
616
|
|
|
572
617
|
export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
|
|
618
|
+
assertSupportedPiVersion(VERSION);
|
|
573
619
|
const reviewSessions = createReviewSessionCoordinator(pi);
|
|
620
|
+
const backgroundWorkflows = new BackgroundWorkflowCoordinator(pi);
|
|
621
|
+
const workflowRuns = new WorkflowRunController(backgroundWorkflows, {
|
|
622
|
+
async resolveWorkflow(name) {
|
|
623
|
+
const { discoverWorkflows } = await loadDiscovery();
|
|
624
|
+
return (await discoverWorkflows(EXTENSION_DIR)).get(name);
|
|
625
|
+
},
|
|
626
|
+
async execute(ctx, name, workflow, options) {
|
|
627
|
+
const perfRecorder = await createInvocationPerf(options);
|
|
628
|
+
const execution = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
|
|
629
|
+
reviewSessions.remember(ctx, execution, options);
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
backgroundWorkflows.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
|
|
574
633
|
registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
|
|
575
|
-
pi
|
|
576
|
-
|
|
634
|
+
registerWorkflowModelProfileCommand(pi);
|
|
635
|
+
registerWorkflowRunCommand(pi, workflowRuns);
|
|
636
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
637
|
+
await backgroundWorkflows.sessionStarted(ctx);
|
|
638
|
+
await workflowRuns.sessionStarted(ctx);
|
|
639
|
+
});
|
|
640
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
641
|
+
await backgroundWorkflows.agentSettled(ctx);
|
|
642
|
+
});
|
|
643
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
644
|
+
workflowRuns.sessionShutdown(ctx);
|
|
645
|
+
await backgroundWorkflows.sessionShutdown(ctx);
|
|
646
|
+
const key = sessionKey(ctx);
|
|
577
647
|
workflowInspections.get(pi)?.delete(key);
|
|
578
648
|
reviewSessions.dispose(ctx);
|
|
579
649
|
});
|
|
@@ -598,11 +668,13 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
598
668
|
});
|
|
599
669
|
|
|
600
670
|
pi.registerCommand("workflow:inspector", {
|
|
601
|
-
description: "Open the current or
|
|
671
|
+
description: "Open the current, last, or a retained run inspector",
|
|
672
|
+
getArgumentCompletions: (argumentPrefix) => workflowRuns.inspectorArgumentCompletions(argumentPrefix),
|
|
602
673
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
603
674
|
const trimmed = args.trim();
|
|
604
675
|
if (trimmed && trimmed !== "last") {
|
|
605
|
-
|
|
676
|
+
if (await workflowRuns.inspectStoredRun(ctx, trimmed)) return;
|
|
677
|
+
ctx.ui.notify(`Workflow run ${trimmed} was not found.`, "warning");
|
|
606
678
|
return;
|
|
607
679
|
}
|
|
608
680
|
await openAvailableWorkflowInspector(pi, ctx);
|
|
@@ -623,6 +695,7 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
623
695
|
// /workflow <name> [args] — user-invoked.
|
|
624
696
|
pi.registerCommand("workflow", {
|
|
625
697
|
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
698
|
+
getArgumentCompletions: workflowArgumentCompletions,
|
|
626
699
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
627
700
|
const direct = parseWorkflowInvocation(args);
|
|
628
701
|
if (direct.optionErrors?.length) {
|
|
@@ -657,12 +730,20 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
657
730
|
},
|
|
658
731
|
});
|
|
659
732
|
|
|
660
|
-
|
|
733
|
+
registerWorkflowTool(pi, reviewSessions, backgroundWorkflows);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
|
|
737
|
+
function registerWorkflowTool(
|
|
738
|
+
pi: ExtensionAPI,
|
|
739
|
+
reviewSessions: ReviewSessionCoordinator,
|
|
740
|
+
backgroundWorkflows: BackgroundWorkflowCoordinator,
|
|
741
|
+
): void {
|
|
661
742
|
pi.registerTool({
|
|
662
743
|
name: "workflow",
|
|
663
744
|
label: "Workflow",
|
|
664
745
|
description:
|
|
665
|
-
"ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/workflow:dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize)
|
|
746
|
+
"ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/workflow:dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize), synchronously by default or explicitly in the background.",
|
|
666
747
|
promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
|
|
667
748
|
promptGuidelines: [
|
|
668
749
|
"Use workflow only when the user opted into workflow orchestration via `dynamax`, `/workflow:dynamax on`, an explicit request to run/author a workflow, or a command/skill instruction.",
|
|
@@ -676,6 +757,9 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
676
757
|
"If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
|
|
677
758
|
"`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
|
|
678
759
|
ADAPTIVE_WORKFLOW_GUIDANCE,
|
|
760
|
+
"Set background: true only when the user explicitly wants the workflow to continue after this tool call; the tool returns a durable run ID and completion is delivered later.",
|
|
761
|
+
"Set autoResumeOnUsageLimit: true only for an explicitly backgrounded workflow when the user wants bounded automatic continuation after a recognized provider usage window.",
|
|
762
|
+
"Set resumeEditedWorkflow: true only with resumeFromRunId when the user explicitly accepts reusing behaviorally identical calls after workflow source edits.",
|
|
679
763
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
680
764
|
],
|
|
681
765
|
parameters: Type.Object({
|
|
@@ -684,6 +768,38 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
684
768
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
685
769
|
concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
|
|
686
770
|
parallelSubmissionLimit: Type.Optional(Type.Number({ description: "Optional limit for eagerly submitted parallel thunks" })),
|
|
771
|
+
maxAgents: Type.Optional(
|
|
772
|
+
Type.Integer({
|
|
773
|
+
description: `Maximum live model calls started across this run; replay hits are excluded; clamped to ${WORKFLOW_MAX_AGENTS_MIN}-${WORKFLOW_MAX_AGENTS_MAX}`,
|
|
774
|
+
}),
|
|
775
|
+
),
|
|
776
|
+
agentTimeoutMs: Type.Optional(
|
|
777
|
+
Type.Integer({
|
|
778
|
+
description: `Maximum live duration per agent in milliseconds; clamped to ${WORKFLOW_AGENT_TIMEOUT_MIN_MS}-${WORKFLOW_AGENT_TIMEOUT_MAX_MS}`,
|
|
779
|
+
}),
|
|
780
|
+
),
|
|
781
|
+
agentRetries: Type.Optional(
|
|
782
|
+
Type.Integer({
|
|
783
|
+
description: `Retries per agent for classified transient provider failures; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
|
|
784
|
+
}),
|
|
785
|
+
),
|
|
786
|
+
autoResumeOnUsageLimit: Type.Optional(
|
|
787
|
+
Type.Boolean({ description: "For a background run, opt into bounded automatic resume after a recognized provider usage limit" }),
|
|
788
|
+
),
|
|
789
|
+
usageLimitMaxAttempts: Type.Optional(
|
|
790
|
+
Type.Integer({
|
|
791
|
+
minimum: WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
|
|
792
|
+
maximum: WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
793
|
+
description: "Maximum total run attempts in one automatic provider-limit resume chain",
|
|
794
|
+
}),
|
|
795
|
+
),
|
|
796
|
+
usageLimitMaxDelayMs: Type.Optional(
|
|
797
|
+
Type.Integer({
|
|
798
|
+
minimum: WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
799
|
+
maximum: WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
|
|
800
|
+
description: "Maximum delay accepted from a provider reset hint before automatic resume",
|
|
801
|
+
}),
|
|
802
|
+
),
|
|
687
803
|
budget: Type.Optional(
|
|
688
804
|
Type.Integer({
|
|
689
805
|
minimum: WORKFLOW_BUDGET_MIN,
|
|
@@ -693,15 +809,20 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
693
809
|
),
|
|
694
810
|
perf: Type.Optional(Type.Boolean({ description: "Include workflow performance timing aggregates in the result details" })),
|
|
695
811
|
resumeFromRunId: Type.Optional(Type.String({ minLength: 1, description: "Workflow run id to resume from by replaying matching completed agent results" })),
|
|
812
|
+
resumeEditedWorkflow: Type.Optional(
|
|
813
|
+
Type.Boolean({ description: "With resumeFromRunId, ignore only workflow-source fingerprint changes while retaining all other replay checks" }),
|
|
814
|
+
),
|
|
815
|
+
background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
|
|
696
816
|
}),
|
|
697
817
|
renderCall(args, theme) {
|
|
698
818
|
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
819
|
+
const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
|
|
699
820
|
if (args.name?.trim()) {
|
|
700
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${suffix}`, 0, 0);
|
|
821
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${background}${suffix}`, 0, 0);
|
|
701
822
|
}
|
|
702
823
|
const preview = compactInlinePreview(args.script);
|
|
703
824
|
const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
|
|
704
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${suffix}${previewSuffix}`, 0, 0);
|
|
825
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${background}${suffix}${previewSuffix}`, 0, 0);
|
|
705
826
|
},
|
|
706
827
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
707
828
|
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
@@ -726,14 +847,31 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
726
847
|
details: { error: "invalid_resume_from_run_id" },
|
|
727
848
|
};
|
|
728
849
|
}
|
|
850
|
+
if (params.resumeEditedWorkflow && !resumeFromRunId) {
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: "resumeEditedWorkflow requires resumeFromRunId." }],
|
|
853
|
+
details: { error: "invalid_edited_workflow_resume" },
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (params.background) {
|
|
857
|
+
const unavailable = backgroundUnavailableResult(ctx.mode);
|
|
858
|
+
if (unavailable) return unavailable;
|
|
859
|
+
}
|
|
729
860
|
|
|
730
861
|
const runOptions = resolveWorkflowRunOptions({
|
|
731
|
-
inspect: ctx.hasUI,
|
|
862
|
+
inspect: ctx.hasUI && ctx.mode === "tui",
|
|
732
863
|
concurrency: params.concurrency,
|
|
733
864
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
865
|
+
maxAgents: params.maxAgents,
|
|
866
|
+
agentTimeoutMs: params.agentTimeoutMs,
|
|
867
|
+
agentRetries: params.agentRetries,
|
|
868
|
+
autoResumeOnUsageLimit: params.autoResumeOnUsageLimit,
|
|
869
|
+
usageLimitMaxAttempts: params.usageLimitMaxAttempts,
|
|
870
|
+
usageLimitMaxDelayMs: params.usageLimitMaxDelayMs,
|
|
734
871
|
budget: params.budget,
|
|
735
872
|
perf: params.perf,
|
|
736
873
|
resumeFromRunId,
|
|
874
|
+
resumeEditedWorkflow: params.resumeEditedWorkflow,
|
|
737
875
|
signal,
|
|
738
876
|
});
|
|
739
877
|
const perfRecorder = await createInvocationPerf(runOptions);
|
|
@@ -765,6 +903,26 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
765
903
|
}
|
|
766
904
|
|
|
767
905
|
const resultArgs = params.args ?? "";
|
|
906
|
+
if (params.background) {
|
|
907
|
+
return await startBackgroundWorkflowTool({
|
|
908
|
+
coordinator: backgroundWorkflows,
|
|
909
|
+
ctx,
|
|
910
|
+
name: resultName,
|
|
911
|
+
options: runOptions,
|
|
912
|
+
async execute(backgroundCtx, backgroundOptions) {
|
|
913
|
+
const execution = await executeResolvedWorkflow(
|
|
914
|
+
pi,
|
|
915
|
+
backgroundCtx,
|
|
916
|
+
resultName,
|
|
917
|
+
mod,
|
|
918
|
+
resultArgs,
|
|
919
|
+
backgroundOptions,
|
|
920
|
+
perfRecorder,
|
|
921
|
+
);
|
|
922
|
+
reviewSessions.remember(ctx, execution, backgroundOptions);
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
}
|
|
768
926
|
const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
|
|
769
927
|
reviewSessions.remember(ctx, execution, runOptions);
|
|
770
928
|
return {
|
|
@@ -59,12 +59,8 @@ export const AdvisoryReportWithStatsSchema = Type.Object({
|
|
|
59
59
|
stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
|
|
63
|
-
export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
|
|
64
|
-
export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
|
|
65
62
|
export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
|
|
66
63
|
export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
|
|
67
|
-
export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
|
|
68
64
|
export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
|
|
69
65
|
export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
|
|
70
66
|
export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
|
|
@@ -37,8 +37,9 @@ export async function executeAgentAttempt(input: {
|
|
|
37
37
|
readonly label: string;
|
|
38
38
|
readonly rowId: number;
|
|
39
39
|
readonly tags: AgentRunTags;
|
|
40
|
+
readonly admitLiveAgent: () => void;
|
|
40
41
|
}): Promise<AgentAttemptResult> {
|
|
41
|
-
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags } = input;
|
|
42
|
+
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags, admitLiveAgent } = input;
|
|
42
43
|
let repositoryBefore: RepositoryResumeContext | undefined;
|
|
43
44
|
let evidence: AgentReplayEvidence | undefined;
|
|
44
45
|
if (isReplayEnabled(replay)) {
|
|
@@ -104,6 +105,7 @@ export async function executeAgentAttempt(input: {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
assertWorkflowBudgetAvailable(rc.budget);
|
|
108
|
+
admitLiveAgent();
|
|
107
109
|
const rawResult = await promptAgentSession({
|
|
108
110
|
rc,
|
|
109
111
|
handle,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { throwIfAborted } from "./cancellation.ts";
|
|
2
|
+
|
|
3
|
+
export class WorkflowAgentLimitError extends Error {
|
|
4
|
+
override readonly name = "WorkflowAgentLimitError";
|
|
5
|
+
readonly code = "WORKFLOW_AGENT_LIMIT_EXCEEDED";
|
|
6
|
+
|
|
7
|
+
constructor(readonly maxAgents: number) {
|
|
8
|
+
super(`Workflow live-agent limit of ${maxAgents} has been reached; no new model request was started.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WorkflowAgentTimeoutError extends Error {
|
|
13
|
+
override readonly name = "WorkflowAgentTimeoutError";
|
|
14
|
+
readonly code = "WORKFLOW_AGENT_TIMEOUT";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
readonly label: string,
|
|
18
|
+
readonly timeoutMs: number,
|
|
19
|
+
) {
|
|
20
|
+
super(`Workflow agent "${label}" exceeded its ${timeoutMs}ms duration limit.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Shared run-level admission counter. Cache hits do not call admit(). */
|
|
25
|
+
export class WorkflowAgentLimiter {
|
|
26
|
+
private admitted = 0;
|
|
27
|
+
|
|
28
|
+
constructor(readonly maxAgents: number) {}
|
|
29
|
+
|
|
30
|
+
admit(signal: AbortSignal | undefined): void {
|
|
31
|
+
throwIfAborted(signal);
|
|
32
|
+
if (this.admitted >= this.maxAgents) throw new WorkflowAgentLimitError(this.maxAgents);
|
|
33
|
+
this.admitted++;
|
|
34
|
+
}
|
|
35
|
+
}
|