pi-workflow-engine 0.10.3 → 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 +418 -213
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
- 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 +10 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
- 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 +96 -6
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -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 +701 -0
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
- 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 +150 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
- 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 +7 -70
- 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/unknown-error.ts +8 -0
- 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 +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -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 +30 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +82 -207
- package/USAGE.md +295 -21
- package/package.json +9 -9
|
@@ -1,32 +1,55 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
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
|
+
import { isAdvisoryReport } from "./src/advisory-schema.ts";
|
|
6
|
+
import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
|
|
7
|
+
import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
|
|
7
8
|
import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
|
|
8
|
-
import
|
|
9
|
+
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
|
|
10
|
+
import type { PerfSink } from "./src/perf.ts";
|
|
9
11
|
import type { WorkflowUsageSnapshot } from "./src/usage.ts";
|
|
10
|
-
import { registerDynamax } from "./src/dynamax.ts";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
12
|
+
import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
|
|
13
|
+
import { sessionKey } from "./src/session-identity.ts";
|
|
14
|
+
import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
|
|
15
|
+
import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
|
|
13
16
|
import {
|
|
14
17
|
formatWorkflowDetailLines,
|
|
15
|
-
isAdvisoryReport,
|
|
16
18
|
isWorkflowResult,
|
|
17
19
|
renderWorkflowResult,
|
|
18
|
-
type WorkflowPerfDetails,
|
|
19
|
-
type WorkflowResultEnvelope,
|
|
20
20
|
} from "./src/ui/workflow-result-renderer.ts";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
parseWorkflowBudgetString,
|
|
23
|
+
parseWorkflowIntegerString,
|
|
24
|
+
resolveWorkflowRunOptions,
|
|
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,
|
|
30
|
+
WORKFLOW_BUDGET_MAX,
|
|
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,
|
|
38
|
+
} from "./src/options.ts";
|
|
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";
|
|
22
47
|
|
|
23
48
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
24
49
|
const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
25
50
|
|
|
26
51
|
function summarize(result: unknown): string {
|
|
27
|
-
if (
|
|
28
|
-
return (result as { summary: string }).summary;
|
|
29
|
-
}
|
|
52
|
+
if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
|
|
30
53
|
return typeof result === "string" ? result : "Workflow finished.";
|
|
31
54
|
}
|
|
32
55
|
|
|
@@ -73,21 +96,6 @@ function formatFindingLocation(location: { readonly file: string; readonly line?
|
|
|
73
96
|
return `${location.file}${line}${symbol}`;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
function workflowEnvelope(
|
|
77
|
-
name: string,
|
|
78
|
-
result: unknown,
|
|
79
|
-
usage?: WorkflowUsageSnapshot,
|
|
80
|
-
perf?: WorkflowPerfDetails,
|
|
81
|
-
metadata?: WorkflowRunMetadata,
|
|
82
|
-
): WorkflowResultEnvelope {
|
|
83
|
-
return { name, result, completedAt: Date.now(), usage, perf, runId: metadata?.runId, resumedFromRunId: metadata?.resumedFromRunId };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
|
|
87
|
-
if (!snapshot?.enabled) return undefined;
|
|
88
|
-
return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
99
|
type DiscoveryModule = typeof import("./src/discovery.ts");
|
|
92
100
|
type EngineModule = typeof import("./src/engine.ts");
|
|
93
101
|
type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
|
|
@@ -104,9 +112,8 @@ async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
|
|
|
104
112
|
return await import("./src/inline-workflow.ts");
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
async function createInvocationPerf(options:
|
|
108
|
-
|
|
109
|
-
if (!enabled) return undefined;
|
|
115
|
+
async function createInvocationPerf(options: ResolvedWorkflowRunOptions): Promise<PerfSink | undefined> {
|
|
116
|
+
if (!options.perf) return undefined;
|
|
110
117
|
const { createPerfRecorder } = await import("./src/perf.ts");
|
|
111
118
|
return createPerfRecorder(true);
|
|
112
119
|
}
|
|
@@ -114,7 +121,7 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
|
|
|
114
121
|
/**
|
|
115
122
|
* Resolve an `api.workflow()` reference to a registered workflow module. Throws on an unknown name.
|
|
116
123
|
*/
|
|
117
|
-
export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<
|
|
124
|
+
export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<LoadedWorkflow> {
|
|
118
125
|
const { discoverWorkflows } = await loadDiscovery();
|
|
119
126
|
const workflows = await discoverWorkflows(EXTENSION_DIR, { perf });
|
|
120
127
|
const mod = workflows.get(ref);
|
|
@@ -128,77 +135,55 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
|
|
|
128
135
|
const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
|
|
129
136
|
const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
|
|
130
137
|
const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
|
|
131
|
-
const WORKFLOW_PICKER_MAX_VISIBLE = 8;
|
|
132
|
-
const WORKFLOW_PICKER_LAYOUT = { minPrimaryColumnWidth: 18, maxPrimaryColumnWidth: 22 };
|
|
133
|
-
|
|
134
|
-
class WorkflowPickerComponent implements Component {
|
|
135
|
-
private readonly list: SelectList;
|
|
136
|
-
|
|
137
|
-
constructor(
|
|
138
|
-
items: SelectItem[],
|
|
139
|
-
private readonly tui: TUI,
|
|
140
|
-
private readonly theme: Theme,
|
|
141
|
-
done: (value: string | undefined) => void,
|
|
142
|
-
) {
|
|
143
|
-
this.list = new SelectList(items, Math.min(WORKFLOW_PICKER_MAX_VISIBLE, items.length), selectListTheme(theme), WORKFLOW_PICKER_LAYOUT);
|
|
144
|
-
this.list.onSelect = (item) => done(item.value);
|
|
145
|
-
this.list.onCancel = () => done(undefined);
|
|
146
|
-
this.list.onSelectionChange = () => this.tui.requestRender();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
render(width: number): string[] {
|
|
150
|
-
const safeWidth = Math.max(20, width);
|
|
151
|
-
const rule = this.theme.fg("borderMuted", "-".repeat(safeWidth));
|
|
152
|
-
const lines = [
|
|
153
|
-
rule,
|
|
154
|
-
this.theme.fg("accent", this.theme.bold("Run workflow")),
|
|
155
|
-
"",
|
|
156
|
-
...this.list.render(safeWidth),
|
|
157
|
-
"",
|
|
158
|
-
this.theme.fg("muted", "up/down navigate enter select escape cancel"),
|
|
159
|
-
rule,
|
|
160
|
-
];
|
|
161
|
-
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
handleInput(data: string): void {
|
|
165
|
-
this.list.handleInput(data);
|
|
166
|
-
this.tui.requestRender();
|
|
167
|
-
}
|
|
168
138
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
);
|
|
171
167
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
function selectListTheme(theme: Theme): SelectListTheme {
|
|
175
|
-
return {
|
|
176
|
-
selectedPrefix: (text) => theme.fg("accent", text),
|
|
177
|
-
selectedText: (text) => theme.fg("accent", text),
|
|
178
|
-
description: (text) => theme.fg("muted", text),
|
|
179
|
-
scrollInfo: (text) => theme.fg("muted", text),
|
|
180
|
-
noMatch: (text) => theme.fg("muted", text),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function workflowPickerItems(workflows: ReadonlyMap<string, WorkflowModule>): SelectItem[] {
|
|
185
|
-
return [
|
|
186
|
-
{ value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL, description: AUTHOR_TEMP_WORKFLOW_DESCRIPTION },
|
|
187
|
-
...[...workflows.values()].map((mod) => ({ value: mod.meta.name, label: mod.meta.name, description: mod.meta.description })),
|
|
188
|
-
];
|
|
168
|
+
return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
|
|
189
169
|
}
|
|
190
170
|
|
|
191
171
|
async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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(
|
|
198
183
|
"Run workflow",
|
|
199
|
-
|
|
184
|
+
choices.map((choice) => choice.display),
|
|
200
185
|
);
|
|
201
|
-
return
|
|
186
|
+
return choices.find((choice) => choice.display === selected)?.value;
|
|
202
187
|
}
|
|
203
188
|
|
|
204
189
|
export interface LastWorkflowInspection {
|
|
@@ -215,30 +200,42 @@ export interface ActiveWorkflowInspection {
|
|
|
215
200
|
readonly snapshot: () => WorkflowProgressSnapshot;
|
|
216
201
|
}
|
|
217
202
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
|
|
222
|
-
return lastWorkflowInspection;
|
|
203
|
+
interface SessionWorkflowInspections {
|
|
204
|
+
last?: LastWorkflowInspection;
|
|
205
|
+
active?: ActiveWorkflowInspection;
|
|
223
206
|
}
|
|
224
207
|
|
|
225
|
-
|
|
226
|
-
|
|
208
|
+
const workflowInspections = new WeakMap<ExtensionAPI, Map<string, SessionWorkflowInspections>>();
|
|
209
|
+
let latestWorkflowInspection: LastWorkflowInspection | undefined;
|
|
210
|
+
let latestActiveWorkflowInspection: ActiveWorkflowInspection | undefined;
|
|
211
|
+
|
|
212
|
+
export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
|
|
213
|
+
return latestWorkflowInspection;
|
|
227
214
|
}
|
|
228
215
|
|
|
229
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
|
+
}
|
|
230
221
|
await ctx.ui.custom<void>(
|
|
231
|
-
(tui, theme, _keybindings, done) => new WorkflowInspector(
|
|
232
|
-
|
|
222
|
+
(tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
|
|
223
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
233
224
|
);
|
|
234
225
|
}
|
|
235
226
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
227
|
+
function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
|
|
228
|
+
const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
|
|
229
|
+
const key = sessionKey(ctx);
|
|
230
|
+
const state = sessions.get(key) ?? {};
|
|
231
|
+
sessions.set(key, state);
|
|
232
|
+
workflowInspections.set(pi, sessions);
|
|
233
|
+
return state;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
237
|
+
const state = workflowInspectionState(pi, ctx);
|
|
238
|
+
const inspection = state.active ?? state.last;
|
|
242
239
|
if (!inspection) {
|
|
243
240
|
ctx.ui.notify("No workflow inspector is available yet", "warning");
|
|
244
241
|
return;
|
|
@@ -246,11 +243,6 @@ async function openAvailableWorkflowInspector(ctx: ExtensionContext): Promise<vo
|
|
|
246
243
|
await openWorkflowInspector(ctx, inspection);
|
|
247
244
|
}
|
|
248
245
|
|
|
249
|
-
function snapshotGetter(inspection: LastWorkflowInspection | ActiveWorkflowInspection): () => WorkflowProgressSnapshot {
|
|
250
|
-
const snapshot = inspection.snapshot;
|
|
251
|
-
return typeof snapshot === "function" ? snapshot : () => snapshot;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
246
|
function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
|
|
255
247
|
return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
|
|
256
248
|
}
|
|
@@ -278,6 +270,10 @@ export function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
|
278
270
|
|
|
279
271
|
const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
|
|
280
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>";
|
|
281
277
|
|
|
282
278
|
function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
|
|
283
279
|
const tokens = input.split(/\s+/).filter(Boolean);
|
|
@@ -299,6 +295,10 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
299
295
|
options.perf = true;
|
|
300
296
|
continue;
|
|
301
297
|
}
|
|
298
|
+
if (token === "--resume-edited") {
|
|
299
|
+
options.resumeEditedWorkflow = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
302
|
if (token === "--result-viewer" || token === "--review-viewer") {
|
|
303
303
|
options.resultViewer = "open";
|
|
304
304
|
continue;
|
|
@@ -327,6 +327,54 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
327
327
|
if (next !== undefined) i++;
|
|
328
328
|
continue;
|
|
329
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
|
+
}
|
|
330
378
|
if (token.startsWith("--budget=")) {
|
|
331
379
|
const parsed = parseBudgetOption(token.slice("--budget=".length));
|
|
332
380
|
if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
|
|
@@ -362,6 +410,7 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
362
410
|
}
|
|
363
411
|
kept.push(token);
|
|
364
412
|
}
|
|
413
|
+
if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
|
|
365
414
|
return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
|
|
366
415
|
}
|
|
367
416
|
|
|
@@ -390,11 +439,12 @@ ${brief.trim()}
|
|
|
390
439
|
Use the workflow tool with a script argument, not a saved workflow name.
|
|
391
440
|
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
392
441
|
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
393
|
-
Set
|
|
442
|
+
Set profile to "small", "medium", or "big" on each agent() call; use explicit model/thinkingLevel only for an intentional override.
|
|
394
443
|
Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
|
|
395
444
|
If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
|
|
396
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.
|
|
397
446
|
Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
|
|
447
|
+
${ADAPTIVE_WORKFLOW_GUIDANCE}
|
|
398
448
|
Do not edit files unless the user explicitly requested edits.`;
|
|
399
449
|
}
|
|
400
450
|
|
|
@@ -402,6 +452,7 @@ export interface WorkflowToolRequestParams {
|
|
|
402
452
|
readonly name?: string;
|
|
403
453
|
readonly script?: string;
|
|
404
454
|
readonly resumeFromRunId?: string;
|
|
455
|
+
readonly background?: boolean;
|
|
405
456
|
}
|
|
406
457
|
|
|
407
458
|
export type WorkflowToolRequest =
|
|
@@ -458,76 +509,152 @@ export async function pickWorkflow(
|
|
|
458
509
|
|
|
459
510
|
export async function sendWorkflowResult(
|
|
460
511
|
pi: ExtensionAPI,
|
|
461
|
-
ctx:
|
|
512
|
+
ctx: ExtensionContext,
|
|
462
513
|
name: string,
|
|
463
|
-
mod:
|
|
514
|
+
mod: LoadedWorkflow,
|
|
464
515
|
args: string,
|
|
465
516
|
options: WorkflowRunOptions,
|
|
466
517
|
perfRecorder?: PerfSink,
|
|
518
|
+
reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
|
|
519
|
+
): Promise<void> {
|
|
520
|
+
await sendResolvedWorkflowResult(
|
|
521
|
+
pi,
|
|
522
|
+
ctx,
|
|
523
|
+
name,
|
|
524
|
+
mod,
|
|
525
|
+
args,
|
|
526
|
+
resolveWorkflowRunOptions(options),
|
|
527
|
+
perfRecorder,
|
|
528
|
+
reviewSessions,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function sendResolvedWorkflowResult(
|
|
533
|
+
pi: ExtensionAPI,
|
|
534
|
+
ctx: ExtensionContext,
|
|
535
|
+
name: string,
|
|
536
|
+
mod: LoadedWorkflow,
|
|
537
|
+
args: string,
|
|
538
|
+
options: ResolvedWorkflowRunOptions,
|
|
539
|
+
perfRecorder: PerfSink | undefined,
|
|
540
|
+
reviewSessions: ReviewSessionCoordinator,
|
|
467
541
|
): Promise<void> {
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
542
|
+
const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
|
|
543
|
+
reviewSessions.remember(ctx, execution, options);
|
|
544
|
+
sendWorkflowExecution(pi, execution);
|
|
545
|
+
await reviewSessions.present(ctx, execution, options);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function executeResolvedWorkflow(
|
|
549
|
+
pi: ExtensionAPI,
|
|
550
|
+
ctx: ExtensionContext,
|
|
551
|
+
name: string,
|
|
552
|
+
mod: LoadedWorkflow,
|
|
553
|
+
args: string,
|
|
554
|
+
options: ResolvedWorkflowRunOptions,
|
|
555
|
+
perfRecorder?: PerfSink,
|
|
556
|
+
): Promise<WorkflowExecution> {
|
|
557
|
+
const { runResolvedWorkflow } = await loadEngine();
|
|
472
558
|
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
559
|
+
const inspections = workflowInspectionState(pi, ctx);
|
|
560
|
+
return await executeWorkflowInvocation({
|
|
561
|
+
ctx,
|
|
562
|
+
name,
|
|
563
|
+
mod,
|
|
564
|
+
args,
|
|
565
|
+
options,
|
|
476
566
|
perfRecorder,
|
|
567
|
+
runResolvedWorkflow,
|
|
477
568
|
resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
|
|
478
569
|
onProgressSource(source) {
|
|
479
570
|
if (source) {
|
|
480
571
|
liveInspection = bindActiveWorkflowInspection(name, args, source);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
572
|
+
inspections.active = liveInspection;
|
|
573
|
+
latestActiveWorkflowInspection = liveInspection;
|
|
574
|
+
} else if (inspections.active === liveInspection) {
|
|
575
|
+
inspections.active = undefined;
|
|
576
|
+
if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
|
|
484
577
|
liveInspection = undefined;
|
|
485
578
|
}
|
|
486
|
-
options.onProgressSource?.(source);
|
|
487
|
-
},
|
|
488
|
-
onPerfSnapshot(snapshot) {
|
|
489
|
-
perfSnapshot = snapshot;
|
|
490
|
-
options.onPerfSnapshot?.(snapshot);
|
|
491
|
-
},
|
|
492
|
-
onUsageSnapshot(snapshot) {
|
|
493
|
-
usageSnapshot = snapshot;
|
|
494
|
-
options.onUsageSnapshot?.(snapshot);
|
|
495
|
-
},
|
|
496
|
-
onRunMetadata(metadata) {
|
|
497
|
-
runMetadata = metadata;
|
|
498
|
-
options.onRunMetadata?.(metadata);
|
|
499
579
|
},
|
|
500
580
|
onProgressSnapshot(snapshot) {
|
|
501
|
-
|
|
502
|
-
|
|
581
|
+
const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
582
|
+
inspections.last = completed;
|
|
583
|
+
latestWorkflowInspection = completed;
|
|
503
584
|
},
|
|
504
585
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
mode: extensionContextMode(ctx),
|
|
510
|
-
hasUI: ctx.hasUI,
|
|
511
|
-
resultViewer: options.resultViewer,
|
|
512
|
-
invocationKind: "command",
|
|
513
|
-
});
|
|
514
|
-
const reviewAction = await maybeShowReviewResultsViewer(ctx, reviewDecision);
|
|
515
|
-
if (reviewDecision.kind !== "send") {
|
|
516
|
-
await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
|
|
517
|
-
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
|
|
589
|
+
const { name } = execution.envelope;
|
|
518
590
|
pi.sendMessage(
|
|
519
591
|
{
|
|
520
592
|
customType: "workflow-result",
|
|
521
|
-
content: formatMessageContent(
|
|
593
|
+
content: formatMessageContent(
|
|
594
|
+
name,
|
|
595
|
+
execution.envelope.result,
|
|
596
|
+
execution.envelope.usage,
|
|
597
|
+
execution.envelope.perf,
|
|
598
|
+
execution.metadata,
|
|
599
|
+
),
|
|
522
600
|
display: true,
|
|
523
|
-
details:
|
|
601
|
+
details: execution.envelope,
|
|
524
602
|
},
|
|
525
603
|
{ triggerTurn: false },
|
|
526
604
|
);
|
|
527
605
|
}
|
|
528
606
|
|
|
529
|
-
|
|
530
|
-
|
|
607
|
+
function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordinator {
|
|
608
|
+
return new ReviewSessionCoordinator(pi, {
|
|
609
|
+
async runFollowUp(ctx, workflow, options) {
|
|
610
|
+
const perfRecorder = await createInvocationPerf(options);
|
|
611
|
+
return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
|
|
612
|
+
},
|
|
613
|
+
publish: (execution) => sendWorkflowExecution(pi, execution),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
|
|
618
|
+
assertSupportedPiVersion(VERSION);
|
|
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));
|
|
633
|
+
registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
|
|
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);
|
|
647
|
+
workflowInspections.get(pi)?.delete(key);
|
|
648
|
+
reviewSessions.dispose(ctx);
|
|
649
|
+
});
|
|
650
|
+
if (shortcuts.results) {
|
|
651
|
+
pi.registerShortcut(shortcuts.results, {
|
|
652
|
+
description: "Open last code-review results",
|
|
653
|
+
handler: async (ctx) => {
|
|
654
|
+
await reviewSessions.reopen(ctx);
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
531
658
|
|
|
532
659
|
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
533
660
|
const details = message.details;
|
|
@@ -541,27 +668,42 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
541
668
|
});
|
|
542
669
|
|
|
543
670
|
pi.registerCommand("workflow:inspector", {
|
|
544
|
-
description: "Open the current or
|
|
671
|
+
description: "Open the current, last, or a retained run inspector",
|
|
672
|
+
getArgumentCompletions: (argumentPrefix) => workflowRuns.inspectorArgumentCompletions(argumentPrefix),
|
|
545
673
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
546
674
|
const trimmed = args.trim();
|
|
547
675
|
if (trimmed && trimmed !== "last") {
|
|
548
|
-
|
|
676
|
+
if (await workflowRuns.inspectStoredRun(ctx, trimmed)) return;
|
|
677
|
+
ctx.ui.notify(`Workflow run ${trimmed} was not found.`, "warning");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
await openAvailableWorkflowInspector(pi, ctx);
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
pi.registerCommand("workflow:results", {
|
|
685
|
+
description: "Reopen the last code-review findings viewer",
|
|
686
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
687
|
+
if (args.trim()) {
|
|
688
|
+
ctx.ui.notify("Usage: /workflow:results", "warning");
|
|
549
689
|
return;
|
|
550
690
|
}
|
|
551
|
-
await
|
|
691
|
+
await reviewSessions.reopen(ctx);
|
|
552
692
|
},
|
|
553
693
|
});
|
|
554
694
|
|
|
555
695
|
// /workflow <name> [args] — user-invoked.
|
|
556
696
|
pi.registerCommand("workflow", {
|
|
557
697
|
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
698
|
+
getArgumentCompletions: workflowArgumentCompletions,
|
|
558
699
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
559
700
|
const direct = parseWorkflowInvocation(args);
|
|
560
701
|
if (direct.optionErrors?.length) {
|
|
561
702
|
ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
|
|
562
703
|
return;
|
|
563
704
|
}
|
|
564
|
-
const
|
|
705
|
+
const directOptions = resolveWorkflowRunOptions(direct.options);
|
|
706
|
+
const perfRecorder = await createInvocationPerf(directOptions);
|
|
565
707
|
const { discoverWorkflows } = await loadDiscovery();
|
|
566
708
|
const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
|
|
567
709
|
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
@@ -583,16 +725,25 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
583
725
|
return;
|
|
584
726
|
}
|
|
585
727
|
|
|
586
|
-
|
|
728
|
+
const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
|
|
729
|
+
await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
|
|
587
730
|
},
|
|
588
731
|
});
|
|
589
732
|
|
|
590
|
-
|
|
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 {
|
|
591
742
|
pi.registerTool({
|
|
592
743
|
name: "workflow",
|
|
593
744
|
label: "Workflow",
|
|
594
745
|
description:
|
|
595
|
-
"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.",
|
|
596
747
|
promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
|
|
597
748
|
promptGuidelines: [
|
|
598
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.",
|
|
@@ -605,6 +756,10 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
605
756
|
"When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
|
|
606
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.",
|
|
607
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.",
|
|
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.",
|
|
608
763
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
609
764
|
],
|
|
610
765
|
parameters: Type.Object({
|
|
@@ -613,6 +768,38 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
613
768
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
614
769
|
concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
|
|
615
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
|
+
),
|
|
616
803
|
budget: Type.Optional(
|
|
617
804
|
Type.Integer({
|
|
618
805
|
minimum: WORKFLOW_BUDGET_MIN,
|
|
@@ -622,15 +809,20 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
622
809
|
),
|
|
623
810
|
perf: Type.Optional(Type.Boolean({ description: "Include workflow performance timing aggregates in the result details" })),
|
|
624
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" })),
|
|
625
816
|
}),
|
|
626
817
|
renderCall(args, theme) {
|
|
627
818
|
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
819
|
+
const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
|
|
628
820
|
if (args.name?.trim()) {
|
|
629
|
-
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);
|
|
630
822
|
}
|
|
631
823
|
const preview = compactInlinePreview(args.script);
|
|
632
824
|
const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
|
|
633
|
-
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);
|
|
634
826
|
},
|
|
635
827
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
636
828
|
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
@@ -655,18 +847,35 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
655
847
|
details: { error: "invalid_resume_from_run_id" },
|
|
656
848
|
};
|
|
657
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
|
+
}
|
|
658
860
|
|
|
659
|
-
const runOptions
|
|
660
|
-
inspect: ctx.hasUI,
|
|
861
|
+
const runOptions = resolveWorkflowRunOptions({
|
|
862
|
+
inspect: ctx.hasUI && ctx.mode === "tui",
|
|
661
863
|
concurrency: params.concurrency,
|
|
662
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,
|
|
663
871
|
budget: params.budget,
|
|
664
872
|
perf: params.perf,
|
|
665
873
|
resumeFromRunId,
|
|
874
|
+
resumeEditedWorkflow: params.resumeEditedWorkflow,
|
|
666
875
|
signal,
|
|
667
|
-
};
|
|
876
|
+
});
|
|
668
877
|
const perfRecorder = await createInvocationPerf(runOptions);
|
|
669
|
-
let mod:
|
|
878
|
+
let mod: LoadedWorkflow;
|
|
670
879
|
let resultName: string;
|
|
671
880
|
|
|
672
881
|
if (request.kind === "named") {
|
|
@@ -693,45 +902,41 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
693
902
|
resultName = mod.meta.name;
|
|
694
903
|
}
|
|
695
904
|
|
|
696
|
-
const { runWorkflow } = await loadEngine();
|
|
697
|
-
let perfSnapshot: PerfSnapshot | undefined;
|
|
698
|
-
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
699
|
-
let runMetadata: WorkflowRunMetadata | undefined;
|
|
700
|
-
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
701
905
|
const resultArgs = params.args ?? "";
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
},
|
|
725
|
-
onProgressSnapshot: (snapshot) => {
|
|
726
|
-
// Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
|
|
727
|
-
// previously uninspectable, unlike the /workflow command path.
|
|
728
|
-
lastWorkflowInspection = { name: resultName, args: resultArgs, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
|
-
const perf = compactPerfSnapshot(perfSnapshot);
|
|
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
|
+
}
|
|
926
|
+
const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
|
|
927
|
+
reviewSessions.remember(ctx, execution, runOptions);
|
|
732
928
|
return {
|
|
733
|
-
content: [{
|
|
734
|
-
|
|
929
|
+
content: [{
|
|
930
|
+
type: "text",
|
|
931
|
+
text: formatMessageContent(
|
|
932
|
+
resultName,
|
|
933
|
+
execution.envelope.result,
|
|
934
|
+
execution.envelope.usage,
|
|
935
|
+
execution.envelope.perf,
|
|
936
|
+
execution.metadata,
|
|
937
|
+
),
|
|
938
|
+
}],
|
|
939
|
+
details: execution.envelope,
|
|
735
940
|
};
|
|
736
941
|
},
|
|
737
942
|
});
|