pi-workflow-engine 0.13.0 → 1.1.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 +152 -416
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/README.md +1 -1
- package/USAGE.md +79 -20
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
import { isAdvisoryReport } from "./src/advisory-schema.ts";
|
|
3
|
+
import { getSelectListTheme, keyText, VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { AutocompleteItem, SelectItem } from "@earendil-works/pi-tui";
|
|
6
5
|
import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import type { WorkflowRunState } from "./src/workflow-run-record.ts";
|
|
7
|
+
import { WORKFLOW_TOOL_NAME, type LoadedWorkflow, type WorkflowModule, type WorkflowProgressSource, type WorkflowRef } from "./src/types.ts";
|
|
8
|
+
import { showWorkflowInspector } from "./src/ui/workflow-inspector.ts";
|
|
9
|
+
import { WorkflowPicker } from "./src/ui/workflow-picker.ts";
|
|
10
10
|
import type { PerfSink } from "./src/perf.ts";
|
|
11
|
-
import type { WorkflowUsageSnapshot } from "./src/usage.ts";
|
|
12
11
|
import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
|
|
13
12
|
import { sessionKey } from "./src/session-identity.ts";
|
|
14
13
|
import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
|
|
15
14
|
import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
|
|
15
|
+
import { isWorkflowResult, renderWorkflowResult, type WorkflowResultHints } from "./src/ui/workflow-result-renderer.ts";
|
|
16
|
+
import { renderWorkflowToolCall, renderWorkflowToolResult } from "./src/ui/workflow-tool-renderer.ts";
|
|
16
17
|
import {
|
|
17
|
-
|
|
18
|
-
isWorkflowResult,
|
|
19
|
-
renderWorkflowResult,
|
|
20
|
-
} from "./src/ui/workflow-result-renderer.ts";
|
|
21
|
-
import {
|
|
22
|
-
parseWorkflowBudgetString,
|
|
23
|
-
parseWorkflowIntegerString,
|
|
18
|
+
parseWorkflowInvocation,
|
|
24
19
|
resolveWorkflowRunOptions,
|
|
25
20
|
type ResolvedWorkflowRunOptions,
|
|
26
21
|
WORKFLOW_AGENT_TIMEOUT_MAX_MS,
|
|
@@ -36,7 +31,8 @@ import {
|
|
|
36
31
|
WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
|
|
37
32
|
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
38
33
|
} from "./src/options.ts";
|
|
39
|
-
import { executeWorkflowInvocation, type
|
|
34
|
+
import { executeWorkflowInvocation, type WorkflowResultEnvelope } from "./src/workflow-execution.ts";
|
|
35
|
+
import { formatWorkflowResultForContext } from "./src/workflow-result-context.ts";
|
|
40
36
|
import { registerWorkflowModelProfileCommand } from "./src/model-profile-command.ts";
|
|
41
37
|
import { BackgroundWorkflowCoordinator } from "./src/background-workflows.ts";
|
|
42
38
|
import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./src/background-workflow-tool.ts";
|
|
@@ -44,58 +40,13 @@ import { registerWorkflowRunCommand, WorkflowRunController } from "./src/workflo
|
|
|
44
40
|
import { completeCurrentArgument, splitArgumentPrefix } from "./src/command-completions.ts";
|
|
45
41
|
import { assertSupportedPiVersion } from "./src/pi-compat.ts";
|
|
46
42
|
import { formatWorkflowInspection, workflowInspectionSnapshot } from "./src/ui/workflow-format.ts";
|
|
43
|
+
import { workflowUsageForPi, type WorkflowUsageSnapshot } from "./src/usage.ts";
|
|
44
|
+
import { unknownErrorMessage } from "./src/unknown-error.ts";
|
|
45
|
+
import { createHostToolBridge } from "./src/host-tools.ts";
|
|
47
46
|
|
|
48
47
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
49
48
|
const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
50
49
|
|
|
51
|
-
function summarize(result: unknown): string {
|
|
52
|
-
if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
|
|
53
|
-
return typeof result === "string" ? result : "Workflow finished.";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function formatMessageContent(
|
|
57
|
-
name: string,
|
|
58
|
-
result: unknown,
|
|
59
|
-
usage?: WorkflowUsageSnapshot,
|
|
60
|
-
perf?: WorkflowPerfDetails,
|
|
61
|
-
metadata?: WorkflowRunMetadata,
|
|
62
|
-
): string {
|
|
63
|
-
const details = formatWorkflowDetailLines({ usage, perf, metadata });
|
|
64
|
-
return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function formatResultForContext(result: unknown): string {
|
|
68
|
-
if (!isAdvisoryReport(result)) return summarize(result);
|
|
69
|
-
|
|
70
|
-
const lines = [result.summary];
|
|
71
|
-
if (result.findings.length > 0) {
|
|
72
|
-
lines.push("", "Findings:");
|
|
73
|
-
result.findings.forEach((finding, index) => {
|
|
74
|
-
const id = `R${String(index + 1).padStart(3, "0")}`;
|
|
75
|
-
lines.push(
|
|
76
|
-
`\n### ${id}: ${finding.summary}`,
|
|
77
|
-
`- Severity: ${finding.severity}`,
|
|
78
|
-
`- Confidence: ${finding.confidence}`,
|
|
79
|
-
`- Category: ${finding.category}`,
|
|
80
|
-
`- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
|
|
81
|
-
`- Impact: ${finding.impact}`,
|
|
82
|
-
`- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
|
|
83
|
-
`- Recommendation: ${finding.recommendation}`,
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (result.nextSteps.length > 0) {
|
|
88
|
-
lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
|
|
89
|
-
}
|
|
90
|
-
return lines.join("\n");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
|
|
94
|
-
const line = location.line === undefined ? "" : `:${location.line}`;
|
|
95
|
-
const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
|
|
96
|
-
return `${location.file}${line}${symbol}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
50
|
type DiscoveryModule = typeof import("./src/discovery.ts");
|
|
100
51
|
type EngineModule = typeof import("./src/engine.ts");
|
|
101
52
|
type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
|
|
@@ -133,7 +84,7 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
|
|
|
133
84
|
}
|
|
134
85
|
|
|
135
86
|
const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
|
|
136
|
-
const AUTHOR_TEMP_WORKFLOW_LABEL = "
|
|
87
|
+
const AUTHOR_TEMP_WORKFLOW_LABEL = "+ author a one-off workflow";
|
|
137
88
|
const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
|
|
138
89
|
|
|
139
90
|
const WORKFLOW_OPTION_COMPLETIONS = [
|
|
@@ -147,7 +98,7 @@ const WORKFLOW_OPTION_COMPLETIONS = [
|
|
|
147
98
|
{ value: "--parallel-limit=", description: "Set the parallel submission limit" },
|
|
148
99
|
{ value: "--max-agents=", description: "Set the maximum admitted live agents" },
|
|
149
100
|
{ value: "--agent-timeout-ms=", description: "Set the timeout for each agent attempt" },
|
|
150
|
-
{ value: "--agent-retries=", description: "
|
|
101
|
+
{ value: "--agent-retries=", description: "Restart an agent after pi's in-session turn retries are exhausted" },
|
|
151
102
|
{ value: "--budget=", description: "Set the workflow output-token budget" },
|
|
152
103
|
{ value: "--resume=", description: "Resume from a retained workflow run ID" },
|
|
153
104
|
] as const;
|
|
@@ -168,21 +119,22 @@ async function workflowArgumentCompletions(argumentPrefix: string): Promise<Auto
|
|
|
168
119
|
return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
|
|
169
120
|
}
|
|
170
121
|
|
|
122
|
+
/** Registered workflows in discovery order (built-ins first), then the author option. */
|
|
171
123
|
async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
124
|
+
const workflowItems: SelectItem[] = [...workflows.values()].map((workflow) => ({
|
|
125
|
+
value: workflow.meta.name,
|
|
126
|
+
label: workflow.meta.name,
|
|
127
|
+
description: workflow.meta.description,
|
|
128
|
+
}));
|
|
129
|
+
if (ctx.mode === "tui") {
|
|
130
|
+
const items = [...workflowItems, { value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL }];
|
|
131
|
+
return await ctx.ui.custom<string | undefined>((...[, theme, , done]) => new WorkflowPicker(items, theme, getSelectListTheme(), done));
|
|
132
|
+
}
|
|
172
133
|
const choices = [
|
|
173
|
-
{
|
|
174
|
-
|
|
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
|
-
})),
|
|
134
|
+
...workflowItems.map((item) => ({ value: item.value, display: `${item.label} — ${item.description}` })),
|
|
135
|
+
{ value: AUTHOR_TEMP_WORKFLOW_VALUE, display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}` },
|
|
181
136
|
];
|
|
182
|
-
const selected = await ctx.ui.select(
|
|
183
|
-
"Run workflow",
|
|
184
|
-
choices.map((choice) => choice.display),
|
|
185
|
-
);
|
|
137
|
+
const selected = await ctx.ui.select("Run a workflow", choices.map((choice) => choice.display));
|
|
186
138
|
return choices.find((choice) => choice.display === selected)?.value;
|
|
187
139
|
}
|
|
188
140
|
|
|
@@ -191,6 +143,7 @@ export interface LastWorkflowInspection {
|
|
|
191
143
|
readonly args: string;
|
|
192
144
|
readonly completedAt: number;
|
|
193
145
|
readonly snapshot: WorkflowProgressSnapshot;
|
|
146
|
+
readonly state?: () => WorkflowRunState;
|
|
194
147
|
}
|
|
195
148
|
|
|
196
149
|
export interface ActiveWorkflowInspection {
|
|
@@ -198,6 +151,7 @@ export interface ActiveWorkflowInspection {
|
|
|
198
151
|
readonly args: string;
|
|
199
152
|
readonly startedAt: number;
|
|
200
153
|
readonly snapshot: () => WorkflowProgressSnapshot;
|
|
154
|
+
readonly state?: () => WorkflowRunState;
|
|
201
155
|
}
|
|
202
156
|
|
|
203
157
|
interface SessionWorkflowInspections {
|
|
@@ -206,22 +160,13 @@ interface SessionWorkflowInspections {
|
|
|
206
160
|
}
|
|
207
161
|
|
|
208
162
|
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;
|
|
214
|
-
}
|
|
215
163
|
|
|
216
164
|
export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
|
|
217
165
|
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
218
166
|
ctx.ui.notify(formatWorkflowInspection(inspection), "info");
|
|
219
167
|
return;
|
|
220
168
|
}
|
|
221
|
-
await ctx.ui.
|
|
222
|
-
(tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
|
|
223
|
-
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
224
|
-
);
|
|
169
|
+
await showWorkflowInspector(ctx.ui, () => workflowInspectionSnapshot(inspection), () => ({ state: inspection.state?.() }));
|
|
225
170
|
}
|
|
226
171
|
|
|
227
172
|
function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
|
|
@@ -244,190 +189,7 @@ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionCo
|
|
|
244
189
|
}
|
|
245
190
|
|
|
246
191
|
function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
|
|
247
|
-
return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export interface WorkflowInvocation {
|
|
251
|
-
name: string;
|
|
252
|
-
args: string;
|
|
253
|
-
options: WorkflowRunOptions;
|
|
254
|
-
refreshDiscovery?: boolean;
|
|
255
|
-
optionErrors?: string[];
|
|
256
|
-
authorBrief?: string;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
260
|
-
const trimmed = input.trim();
|
|
261
|
-
const space = trimmed.indexOf(" ");
|
|
262
|
-
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
263
|
-
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
264
|
-
const { args, options, refreshDiscovery, optionErrors } = parseWorkflowOptions(rest);
|
|
265
|
-
const invocation: WorkflowInvocation = { name, args, options };
|
|
266
|
-
if (refreshDiscovery) invocation.refreshDiscovery = refreshDiscovery;
|
|
267
|
-
if (optionErrors) invocation.optionErrors = optionErrors;
|
|
268
|
-
return invocation;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
|
|
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>";
|
|
277
|
-
|
|
278
|
-
function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
|
|
279
|
-
const tokens = input.split(/\s+/).filter(Boolean);
|
|
280
|
-
const kept: string[] = [];
|
|
281
|
-
const options: WorkflowRunOptions = {};
|
|
282
|
-
const optionErrors: string[] = [];
|
|
283
|
-
let refreshDiscovery = false;
|
|
284
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
285
|
-
const token = tokens[i];
|
|
286
|
-
if (token === "--inspect") {
|
|
287
|
-
options.inspect = true;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (token === "--refresh") {
|
|
291
|
-
refreshDiscovery = true;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (token === "--perf") {
|
|
295
|
-
options.perf = true;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (token === "--resume-edited") {
|
|
299
|
-
options.resumeEditedWorkflow = true;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (token === "--result-viewer" || token === "--review-viewer") {
|
|
303
|
-
options.resultViewer = "open";
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (token === "--no-result-viewer" || token === "--no-review-viewer") {
|
|
307
|
-
options.resultViewer = "skip";
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
if (token.startsWith("--concurrency=")) {
|
|
311
|
-
options.concurrency = parseNumericOption(token.slice("--concurrency=".length));
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
if (token === "--concurrency") {
|
|
315
|
-
const next = tokens[i + 1];
|
|
316
|
-
options.concurrency = parseNumericOption(next);
|
|
317
|
-
if (next !== undefined) i++;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (token.startsWith("--parallel-limit=")) {
|
|
321
|
-
options.parallelSubmissionLimit = parseNumericOption(token.slice("--parallel-limit=".length));
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (token === "--parallel-limit") {
|
|
325
|
-
const next = tokens[i + 1];
|
|
326
|
-
options.parallelSubmissionLimit = parseNumericOption(next);
|
|
327
|
-
if (next !== undefined) i++;
|
|
328
|
-
continue;
|
|
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
|
-
}
|
|
378
|
-
if (token.startsWith("--budget=")) {
|
|
379
|
-
const parsed = parseBudgetOption(token.slice("--budget=".length));
|
|
380
|
-
if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
|
|
381
|
-
else options.budget = parsed;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (token === "--budget") {
|
|
385
|
-
const next = tokens[i + 1];
|
|
386
|
-
const parsed = next === undefined ? undefined : parseBudgetOption(next);
|
|
387
|
-
if (parsed === undefined) {
|
|
388
|
-
optionErrors.push(INVALID_BUDGET_OPTION);
|
|
389
|
-
} else {
|
|
390
|
-
options.budget = parsed;
|
|
391
|
-
i++;
|
|
392
|
-
}
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if (token.startsWith("--resume=")) {
|
|
396
|
-
const value = token.slice("--resume=".length).trim();
|
|
397
|
-
if (value === "") optionErrors.push(INVALID_RESUME_OPTION);
|
|
398
|
-
else options.resumeFromRunId = value;
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (token === "--resume") {
|
|
402
|
-
const next = tokens[i + 1];
|
|
403
|
-
if (next === undefined || next.startsWith("--")) {
|
|
404
|
-
optionErrors.push(INVALID_RESUME_OPTION);
|
|
405
|
-
} else {
|
|
406
|
-
options.resumeFromRunId = next;
|
|
407
|
-
i++;
|
|
408
|
-
}
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
kept.push(token);
|
|
412
|
-
}
|
|
413
|
-
if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
|
|
414
|
-
return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function parseBudgetOption(value: string): number | undefined {
|
|
418
|
-
return parseWorkflowBudgetString(value);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function parseNumericOption(value: string | undefined): number | undefined {
|
|
422
|
-
if (value === undefined) return undefined;
|
|
423
|
-
const parsed = Number(value);
|
|
424
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function compactInlinePreview(script: string | undefined): string {
|
|
428
|
-
if (!script) return "";
|
|
429
|
-
const compact = script.replace(/\s+/g, " ").trim();
|
|
430
|
-
return compact.length > 60 ? `${compact.slice(0, 57)}…` : compact;
|
|
192
|
+
return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot(), state: source.state };
|
|
431
193
|
}
|
|
432
194
|
|
|
433
195
|
export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
|
|
@@ -436,15 +198,7 @@ export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
|
|
|
436
198
|
User brief:
|
|
437
199
|
${brief.trim()}
|
|
438
200
|
|
|
439
|
-
Use the workflow tool with a script argument, not a saved workflow name.
|
|
440
|
-
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
441
|
-
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
442
|
-
Set profile to "small", "medium", or "big" on each agent() call; use explicit model/thinkingLevel only for an intentional override.
|
|
443
|
-
Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
|
|
444
|
-
If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
|
|
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.
|
|
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}
|
|
201
|
+
Use the workflow tool with a script argument, not a saved workflow name, and follow its guidelines for inline scripts.
|
|
448
202
|
Do not edit files unless the user explicitly requested edits.`;
|
|
449
203
|
}
|
|
450
204
|
|
|
@@ -460,9 +214,17 @@ export type WorkflowToolRequest =
|
|
|
460
214
|
| { readonly kind: "inline"; readonly script: string }
|
|
461
215
|
| { readonly kind: "error"; readonly error: "invalid_workflow_invocation"; readonly message: string };
|
|
462
216
|
|
|
463
|
-
export interface WorkflowToolErrorResult {
|
|
217
|
+
export interface WorkflowToolErrorResult<TDetails = WorkflowToolErrorDetails> {
|
|
464
218
|
readonly content: Array<{ readonly type: "text"; readonly text: string }>;
|
|
465
|
-
readonly details:
|
|
219
|
+
readonly details: TDetails;
|
|
220
|
+
/** The model sees a failed call while the renderer still reads `details`. */
|
|
221
|
+
readonly isError: true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type WorkflowToolErrorDetails = { readonly error: "invalid_workflow_invocation" } | { readonly error: "inline_compile_error"; readonly message: string };
|
|
225
|
+
|
|
226
|
+
function workflowToolError<TDetails>(text: string, details: TDetails): WorkflowToolErrorResult<TDetails> {
|
|
227
|
+
return { content: [{ type: "text", text }], details, isError: true };
|
|
466
228
|
}
|
|
467
229
|
|
|
468
230
|
const INVALID_WORKFLOW_INVOCATION_MESSAGE = "Provide exactly one workflow name or inline workflow script.";
|
|
@@ -477,34 +239,37 @@ export function normalizeWorkflowToolRequest(params: WorkflowToolRequestParams):
|
|
|
477
239
|
}
|
|
478
240
|
|
|
479
241
|
export function invalidWorkflowInvocationResult(): WorkflowToolErrorResult {
|
|
480
|
-
return
|
|
242
|
+
return workflowToolError(INVALID_WORKFLOW_INVOCATION_MESSAGE, { error: "invalid_workflow_invocation" });
|
|
481
243
|
}
|
|
482
244
|
|
|
483
245
|
export function inlineCompileErrorResult(message: string): WorkflowToolErrorResult {
|
|
484
|
-
return
|
|
246
|
+
return workflowToolError(`Inline workflow did not compile: ${message}`, { error: "inline_compile_error", message });
|
|
485
247
|
}
|
|
486
248
|
|
|
249
|
+
export type WorkflowPickerSelection =
|
|
250
|
+
| { readonly kind: "run"; readonly name: string; readonly args: string }
|
|
251
|
+
| { readonly kind: "author"; readonly brief: string };
|
|
252
|
+
|
|
487
253
|
export async function pickWorkflow(
|
|
488
254
|
workflows: ReadonlyMap<string, WorkflowModule>,
|
|
489
255
|
ctx: ExtensionCommandContext,
|
|
490
|
-
): Promise<
|
|
491
|
-
const
|
|
492
|
-
if (!
|
|
256
|
+
): Promise<WorkflowPickerSelection | undefined> {
|
|
257
|
+
const name = await selectWorkflowValue(workflows, ctx);
|
|
258
|
+
if (!name) return undefined;
|
|
493
259
|
|
|
494
|
-
if (
|
|
260
|
+
if (name === AUTHOR_TEMP_WORKFLOW_VALUE) {
|
|
495
261
|
const brief = await ctx.ui.editor(
|
|
496
262
|
"Describe temporary workflow",
|
|
497
263
|
"Goal:\n\nAgents to run:\n- \n\nFinal output should include:\n- summary\n- findings\n- next steps\n",
|
|
498
264
|
);
|
|
499
265
|
const trimmed = brief?.trim();
|
|
500
|
-
|
|
501
|
-
return { name: "", args: "", options: {}, authorBrief: trimmed };
|
|
266
|
+
return trimmed ? { kind: "author", brief: trimmed } : undefined;
|
|
502
267
|
}
|
|
503
268
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
return { name, args
|
|
269
|
+
if (name !== "code-review") return { kind: "run", name, args: "" };
|
|
270
|
+
// Escape resolves undefined and cancels; only a submitted blank target means auto-detect.
|
|
271
|
+
const target = await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff");
|
|
272
|
+
return target === undefined ? undefined : { kind: "run", name, args: target.trim() };
|
|
508
273
|
}
|
|
509
274
|
|
|
510
275
|
export async function sendWorkflowResult(
|
|
@@ -513,36 +278,14 @@ export async function sendWorkflowResult(
|
|
|
513
278
|
name: string,
|
|
514
279
|
mod: LoadedWorkflow,
|
|
515
280
|
args: string,
|
|
516
|
-
options:
|
|
281
|
+
options: ResolvedWorkflowRunOptions,
|
|
517
282
|
perfRecorder?: PerfSink,
|
|
518
283
|
reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
|
|
519
284
|
): Promise<void> {
|
|
520
|
-
await
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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,
|
|
541
|
-
): Promise<void> {
|
|
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);
|
|
285
|
+
const envelope = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
|
|
286
|
+
const retained = reviewSessions.remember(ctx, envelope, options);
|
|
287
|
+
publishWorkflowResult(pi, envelope);
|
|
288
|
+
await reviewSessions.present(ctx, retained, options);
|
|
546
289
|
}
|
|
547
290
|
|
|
548
291
|
async function executeResolvedWorkflow(
|
|
@@ -553,7 +296,7 @@ async function executeResolvedWorkflow(
|
|
|
553
296
|
args: string,
|
|
554
297
|
options: ResolvedWorkflowRunOptions,
|
|
555
298
|
perfRecorder?: PerfSink,
|
|
556
|
-
): Promise<
|
|
299
|
+
): Promise<WorkflowResultEnvelope> {
|
|
557
300
|
const { runResolvedWorkflow } = await loadEngine();
|
|
558
301
|
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
559
302
|
const inspections = workflowInspectionState(pi, ctx);
|
|
@@ -570,35 +313,25 @@ async function executeResolvedWorkflow(
|
|
|
570
313
|
if (source) {
|
|
571
314
|
liveInspection = bindActiveWorkflowInspection(name, args, source);
|
|
572
315
|
inspections.active = liveInspection;
|
|
573
|
-
latestActiveWorkflowInspection = liveInspection;
|
|
574
316
|
} else if (inspections.active === liveInspection) {
|
|
575
317
|
inspections.active = undefined;
|
|
576
|
-
if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
|
|
577
318
|
liveInspection = undefined;
|
|
578
319
|
}
|
|
579
320
|
},
|
|
580
321
|
onProgressSnapshot(snapshot) {
|
|
581
|
-
|
|
582
|
-
inspections.last =
|
|
583
|
-
latestWorkflowInspection = completed;
|
|
322
|
+
// The durable state settles after this final snapshot; keep reading it from the live source.
|
|
323
|
+
inspections.last = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot, state: liveInspection?.state };
|
|
584
324
|
},
|
|
585
325
|
});
|
|
586
326
|
}
|
|
587
327
|
|
|
588
|
-
function
|
|
589
|
-
const { name } = execution.envelope;
|
|
328
|
+
function publishWorkflowResult(pi: ExtensionAPI, envelope: WorkflowResultEnvelope): void {
|
|
590
329
|
pi.sendMessage(
|
|
591
330
|
{
|
|
592
331
|
customType: "workflow-result",
|
|
593
|
-
content:
|
|
594
|
-
name,
|
|
595
|
-
execution.envelope.result,
|
|
596
|
-
execution.envelope.usage,
|
|
597
|
-
execution.envelope.perf,
|
|
598
|
-
execution.metadata,
|
|
599
|
-
),
|
|
332
|
+
content: formatWorkflowResultForContext(envelope),
|
|
600
333
|
display: true,
|
|
601
|
-
details:
|
|
334
|
+
details: envelope,
|
|
602
335
|
},
|
|
603
336
|
{ triggerTurn: false },
|
|
604
337
|
);
|
|
@@ -610,7 +343,7 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
|
|
|
610
343
|
const perfRecorder = await createInvocationPerf(options);
|
|
611
344
|
return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
|
|
612
345
|
},
|
|
613
|
-
publish: (
|
|
346
|
+
publish: (envelope) => publishWorkflowResult(pi, envelope),
|
|
614
347
|
});
|
|
615
348
|
}
|
|
616
349
|
|
|
@@ -625,12 +358,12 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
625
358
|
},
|
|
626
359
|
async execute(ctx, name, workflow, options) {
|
|
627
360
|
const perfRecorder = await createInvocationPerf(options);
|
|
628
|
-
const
|
|
629
|
-
reviewSessions.remember(ctx,
|
|
361
|
+
const envelope = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
|
|
362
|
+
reviewSessions.remember(ctx, envelope, options);
|
|
630
363
|
},
|
|
631
364
|
});
|
|
632
365
|
backgroundWorkflows.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
|
|
633
|
-
registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
|
|
366
|
+
const dynamax = registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
|
|
634
367
|
registerWorkflowModelProfileCommand(pi);
|
|
635
368
|
registerWorkflowRunCommand(pi, workflowRuns);
|
|
636
369
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -658,13 +391,8 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
658
391
|
|
|
659
392
|
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
660
393
|
const details = message.details;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
runId: details.runId,
|
|
664
|
-
resumedFromRunId: details.resumedFromRunId,
|
|
665
|
-
}, details.perf);
|
|
666
|
-
}
|
|
667
|
-
return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
|
|
394
|
+
const view = isWorkflowResult(details) ? details : { name: "workflow", result: details ?? message.content };
|
|
395
|
+
return renderWorkflowResult(view, expanded, theme, workflowResultHints(shortcuts));
|
|
668
396
|
});
|
|
669
397
|
|
|
670
398
|
pi.registerCommand("workflow:inspector", {
|
|
@@ -697,40 +425,54 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
697
425
|
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
698
426
|
getArgumentCompletions: workflowArgumentCompletions,
|
|
699
427
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
ctx.ui.notify(`Invalid workflow option: ${
|
|
428
|
+
const invocation = parseWorkflowInvocation(args);
|
|
429
|
+
if (invocation.optionErrors.length > 0) {
|
|
430
|
+
ctx.ui.notify(`Invalid workflow option: ${invocation.optionErrors.join("; ")}`, "warning");
|
|
703
431
|
return;
|
|
704
432
|
}
|
|
705
|
-
const
|
|
706
|
-
const perfRecorder = await createInvocationPerf(
|
|
433
|
+
const options = resolveWorkflowRunOptions(invocation.options);
|
|
434
|
+
const perfRecorder = await createInvocationPerf(options);
|
|
707
435
|
const { discoverWorkflows } = await loadDiscovery();
|
|
708
|
-
const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh:
|
|
436
|
+
const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: invocation.refreshDiscovery, perf: perfRecorder });
|
|
709
437
|
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (!invocation) {
|
|
438
|
+
if (!invocation.name && !ctx.hasUI) {
|
|
713
439
|
ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
|
|
714
440
|
return;
|
|
715
441
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
442
|
+
const selection: WorkflowPickerSelection | undefined = invocation.name
|
|
443
|
+
? { kind: "run", name: invocation.name, args: invocation.args }
|
|
444
|
+
: await pickWorkflow(workflows, ctx);
|
|
445
|
+
// Dismissing the picker or one of its follow-up prompts is a deliberate cancel, not a usage error.
|
|
446
|
+
if (!selection) return;
|
|
447
|
+
|
|
448
|
+
if (selection.kind === "author") {
|
|
449
|
+
// A send while the agent streams is rejected, which would leave the one-shot armed for an unrelated prompt.
|
|
450
|
+
await ctx.waitForIdle();
|
|
451
|
+
dynamax.markOneShot(ctx);
|
|
452
|
+
pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(selection.brief));
|
|
719
453
|
return;
|
|
720
454
|
}
|
|
721
455
|
|
|
722
|
-
const mod = workflows.get(
|
|
456
|
+
const mod = workflows.get(selection.name);
|
|
723
457
|
if (!mod) {
|
|
724
|
-
ctx.ui.notify(`Unknown workflow "${
|
|
458
|
+
ctx.ui.notify(`Unknown workflow "${selection.name}". Available: ${available}`, "error");
|
|
725
459
|
return;
|
|
726
460
|
}
|
|
727
461
|
|
|
728
|
-
|
|
729
|
-
await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
|
|
462
|
+
await sendWorkflowResult(pi, ctx, selection.name, mod, selection.args, options, perfRecorder, reviewSessions);
|
|
730
463
|
},
|
|
731
464
|
});
|
|
732
465
|
|
|
733
|
-
registerWorkflowTool(pi, reviewSessions, backgroundWorkflows);
|
|
466
|
+
registerWorkflowTool(pi, reviewSessions, backgroundWorkflows, shortcuts);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Result footers advertise the configured triage and inspector shortcuts and pi's current expand key, read at render time. */
|
|
470
|
+
function workflowResultHints(shortcuts: DynamaxShortcuts): WorkflowResultHints {
|
|
471
|
+
return {
|
|
472
|
+
triage: shortcuts.results ?? "/workflow:results",
|
|
473
|
+
inspect: shortcuts.inspector ?? "/workflow:inspector",
|
|
474
|
+
expand: keyText("app.tools.expand") || undefined,
|
|
475
|
+
};
|
|
734
476
|
}
|
|
735
477
|
|
|
736
478
|
/** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
|
|
@@ -738,13 +480,18 @@ function registerWorkflowTool(
|
|
|
738
480
|
pi: ExtensionAPI,
|
|
739
481
|
reviewSessions: ReviewSessionCoordinator,
|
|
740
482
|
backgroundWorkflows: BackgroundWorkflowCoordinator,
|
|
483
|
+
shortcuts: DynamaxShortcuts,
|
|
741
484
|
): void {
|
|
742
485
|
pi.registerTool({
|
|
743
|
-
name:
|
|
486
|
+
name: WORKFLOW_TOOL_NAME,
|
|
744
487
|
label: "Workflow",
|
|
745
488
|
description:
|
|
746
489
|
"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.",
|
|
747
490
|
promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
|
|
491
|
+
// Like codemode itself, an orchestrating tool stays a top-level model action: from a codemode script a
|
|
492
|
+
// run would lose its tool row, hit the script's output cap, and parallel runs would each get their own
|
|
493
|
+
// concurrency cap. It also stays declared under `codemode.mode: "only"`, which hides direct tools.
|
|
494
|
+
exposure: "model-only",
|
|
748
495
|
promptGuidelines: [
|
|
749
496
|
"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.",
|
|
750
497
|
"Use workflow with `name` for existing registered workflows such as code-review, diagnose, refactor-scout, or perf-review.",
|
|
@@ -752,9 +499,13 @@ function registerWorkflowTool(
|
|
|
752
499
|
"Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
|
|
753
500
|
"Inline scripts may compose registered workflows in-process via `api.workflow(\"<name>\", args)` (e.g. `await api.workflow(\"code-review\", \"HEAD~3\")`); it returns the sub-workflow's result and nests one level only.",
|
|
754
501
|
"Subagents receive no skills by default. In inline workflows, pass `skills: [\"skill-name\"]` per `agent()` call when the user asks for a skill or a stage should use one; grant only the needed skills.",
|
|
502
|
+
"Set `profile: \"small\"`, `\"medium\"`, or `\"big\"` on every `api.agent()` call so the user's central model routes apply; use `model`/`thinkingLevel` only for an intentional override.",
|
|
755
503
|
"Always pass a plain string as the first `api.agent()` argument; build prompts with template strings before calling agent().",
|
|
756
504
|
"When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
|
|
757
505
|
"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.",
|
|
506
|
+
"Add \"codemode\" to an inline agent's `tools` when it should batch or filter many tool calls in one script; its scripts can call only that agent's other allowed tools.",
|
|
507
|
+
"`api.classify({ state, questions }, { label })` answers typed choice/score/bool questions with a classifier model and no chat session; use it for cheap gates such as dedup or triage, and handle its rejection when no classifier is configured.",
|
|
508
|
+
"Host MCP tools reach subagents only in synchronous workflow tool runs: name them in `tools` (e.g. `mcp__server__tool`), or use `toolHints: [\"external-search\"]` to pick up MCP web research tools.",
|
|
758
509
|
"`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
510
|
ADAPTIVE_WORKFLOW_GUIDANCE,
|
|
760
511
|
"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.",
|
|
@@ -780,7 +531,7 @@ function registerWorkflowTool(
|
|
|
780
531
|
),
|
|
781
532
|
agentRetries: Type.Optional(
|
|
782
533
|
Type.Integer({
|
|
783
|
-
description: `
|
|
534
|
+
description: `Whole-agent restarts after a transient provider failure that pi's in-session turn retry (the user's pi retry settings) could not recover; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
|
|
784
535
|
}),
|
|
785
536
|
),
|
|
786
537
|
autoResumeOnUsageLimit: Type.Optional(
|
|
@@ -815,43 +566,20 @@ function registerWorkflowTool(
|
|
|
815
566
|
background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
|
|
816
567
|
}),
|
|
817
568
|
renderCall(args, theme) {
|
|
818
|
-
|
|
819
|
-
const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
|
|
820
|
-
if (args.name?.trim()) {
|
|
821
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${background}${suffix}`, 0, 0);
|
|
822
|
-
}
|
|
823
|
-
const preview = compactInlinePreview(args.script);
|
|
824
|
-
const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
|
|
825
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${background}${suffix}${previewSuffix}`, 0, 0);
|
|
569
|
+
return renderWorkflowToolCall(args, theme);
|
|
826
570
|
},
|
|
827
|
-
renderResult(result,
|
|
828
|
-
|
|
829
|
-
const details = result.details;
|
|
830
|
-
if (isWorkflowResult(details)) {
|
|
831
|
-
return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
|
|
832
|
-
runId: details.runId,
|
|
833
|
-
resumedFromRunId: details.resumedFromRunId,
|
|
834
|
-
}, details.perf);
|
|
835
|
-
}
|
|
836
|
-
const first = result.content[0];
|
|
837
|
-
const text = first?.type === "text" ? first.text : "Workflow finished.";
|
|
838
|
-
return new Text(theme.fg("muted", text), 0, 0);
|
|
571
|
+
renderResult(result, options, theme, context) {
|
|
572
|
+
return renderWorkflowToolResult(result, options, theme, context.args, workflowResultHints(shortcuts));
|
|
839
573
|
},
|
|
840
|
-
async execute(_toolCallId, params, signal,
|
|
574
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
841
575
|
const request = normalizeWorkflowToolRequest(params);
|
|
842
576
|
if (request.kind === "error") return invalidWorkflowInvocationResult();
|
|
843
577
|
const resumeFromRunId = params.resumeFromRunId?.trim();
|
|
844
578
|
if (params.resumeFromRunId !== undefined && resumeFromRunId === "") {
|
|
845
|
-
return {
|
|
846
|
-
content: [{ type: "text", text: "resumeFromRunId must be non-empty." }],
|
|
847
|
-
details: { error: "invalid_resume_from_run_id" },
|
|
848
|
-
};
|
|
579
|
+
return workflowToolError("resumeFromRunId must be non-empty.", { error: "invalid_resume_from_run_id" });
|
|
849
580
|
}
|
|
850
581
|
if (params.resumeEditedWorkflow && !resumeFromRunId) {
|
|
851
|
-
return {
|
|
852
|
-
content: [{ type: "text", text: "resumeEditedWorkflow requires resumeFromRunId." }],
|
|
853
|
-
details: { error: "invalid_edited_workflow_resume" },
|
|
854
|
-
};
|
|
582
|
+
return workflowToolError("resumeEditedWorkflow requires resumeFromRunId.", { error: "invalid_edited_workflow_resume" });
|
|
855
583
|
}
|
|
856
584
|
if (params.background) {
|
|
857
585
|
const unavailable = backgroundUnavailableResult(ctx.mode);
|
|
@@ -859,7 +587,6 @@ function registerWorkflowTool(
|
|
|
859
587
|
}
|
|
860
588
|
|
|
861
589
|
const runOptions = resolveWorkflowRunOptions({
|
|
862
|
-
inspect: ctx.hasUI && ctx.mode === "tui",
|
|
863
590
|
concurrency: params.concurrency,
|
|
864
591
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
865
592
|
maxAgents: params.maxAgents,
|
|
@@ -884,10 +611,7 @@ function registerWorkflowTool(
|
|
|
884
611
|
const named = workflows.get(request.name);
|
|
885
612
|
if (!named) {
|
|
886
613
|
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
887
|
-
return {
|
|
888
|
-
content: [{ type: "text", text: `Unknown workflow "${request.name}". Available: ${available}` }],
|
|
889
|
-
details: { error: "unknown_workflow", available },
|
|
890
|
-
};
|
|
614
|
+
return workflowToolError(`Unknown workflow "${request.name}". Available: ${available}`, { error: "unknown_workflow", available });
|
|
891
615
|
}
|
|
892
616
|
mod = named;
|
|
893
617
|
resultName = request.name;
|
|
@@ -910,7 +634,7 @@ function registerWorkflowTool(
|
|
|
910
634
|
name: resultName,
|
|
911
635
|
options: runOptions,
|
|
912
636
|
async execute(backgroundCtx, backgroundOptions) {
|
|
913
|
-
const
|
|
637
|
+
const envelope = await executeResolvedWorkflow(
|
|
914
638
|
pi,
|
|
915
639
|
backgroundCtx,
|
|
916
640
|
resultName,
|
|
@@ -919,24 +643,36 @@ function registerWorkflowTool(
|
|
|
919
643
|
backgroundOptions,
|
|
920
644
|
perfRecorder,
|
|
921
645
|
);
|
|
922
|
-
reviewSessions.remember(ctx,
|
|
646
|
+
reviewSessions.remember(ctx, envelope, backgroundOptions);
|
|
923
647
|
},
|
|
924
648
|
});
|
|
925
649
|
}
|
|
926
|
-
|
|
927
|
-
|
|
650
|
+
// Marks the tool row as running (rendered `● running <name>`) until the envelope replaces it.
|
|
651
|
+
onUpdate?.({ content: [{ type: "text", text: `Running workflow ${resultName}.` }], details: { state: "running", name: resultName } });
|
|
652
|
+
// Only this synchronous path may bridge host MCP tools: executeTool ends with the tool call.
|
|
653
|
+
const hostTools = createHostToolBridge(() => pi.getAllTools(), ctx);
|
|
654
|
+
// Keep the latest usage: a failed run still reports what its agents spent instead of dropping it with the error.
|
|
655
|
+
let spent: WorkflowUsageSnapshot | undefined;
|
|
656
|
+
const onUsageSnapshot = (snapshot: WorkflowUsageSnapshot) => {
|
|
657
|
+
spent = snapshot;
|
|
658
|
+
return runOptions.onUsageSnapshot?.(snapshot);
|
|
659
|
+
};
|
|
660
|
+
let envelope: WorkflowResultEnvelope;
|
|
661
|
+
try {
|
|
662
|
+
envelope = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, { ...runOptions, hostTools, onUsageSnapshot }, perfRecorder);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
const message = unknownErrorMessage(error);
|
|
665
|
+
return {
|
|
666
|
+
...workflowToolError(`Workflow ${resultName} failed: ${message}`, { error: "workflow_failed", name: resultName, message }),
|
|
667
|
+
usage: workflowUsageForPi(spent),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
reviewSessions.remember(ctx, envelope, runOptions);
|
|
928
671
|
return {
|
|
929
|
-
content: [{
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
execution.envelope.result,
|
|
934
|
-
execution.envelope.usage,
|
|
935
|
-
execution.envelope.perf,
|
|
936
|
-
execution.metadata,
|
|
937
|
-
),
|
|
938
|
-
}],
|
|
939
|
-
details: execution.envelope,
|
|
672
|
+
content: [{ type: "text", text: formatWorkflowResultForContext(envelope) }],
|
|
673
|
+
details: envelope,
|
|
674
|
+
// pi counts tool-result usage toward the session cost; subagent spend was invisible there before.
|
|
675
|
+
usage: workflowUsageForPi(envelope.usage),
|
|
940
676
|
};
|
|
941
677
|
},
|
|
942
678
|
});
|