@tt-a1i/openpi 0.3.1 → 0.5.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/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* `agent()` always resolves to `{ ok, output, structured?, error? }` — it
|
|
18
18
|
* never throws into the script. Scripts branch on `ok` explicitly.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
20
|
+
* Interactive runs detach by default and deliver a completion turn later.
|
|
21
|
+
* Pass `wait: true` when the current tool call must return the final result.
|
|
22
|
+
* Run artifacts (script, args, statuses, result) are saved
|
|
23
23
|
* under `~/.pi/agent/workflows/<runId>/` for inspection; result and bounded
|
|
24
24
|
* transcripts use separate artifacts.
|
|
25
25
|
*
|
|
@@ -33,43 +33,90 @@ import { randomBytes } from "node:crypto";
|
|
|
33
33
|
import * as fs from "node:fs";
|
|
34
34
|
import * as path from "node:path";
|
|
35
35
|
import {
|
|
36
|
+
type ExtensionAPI,
|
|
37
|
+
type ExtensionContext,
|
|
36
38
|
getAgentDir,
|
|
37
39
|
getMarkdownTheme,
|
|
38
40
|
keyHint,
|
|
39
|
-
type ExtensionAPI,
|
|
40
41
|
type SessionManager,
|
|
41
|
-
type ExtensionContext,
|
|
42
42
|
} from "@earendil-works/pi-coding-agent";
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
import {
|
|
44
|
+
Container,
|
|
45
|
+
Markdown,
|
|
46
|
+
Spacer,
|
|
47
|
+
Text,
|
|
48
|
+
truncateToWidth,
|
|
49
|
+
} from "@earendil-works/pi-tui";
|
|
50
|
+
import { type Static, Type } from "typebox";
|
|
51
|
+
import {
|
|
52
|
+
createStatusWriter,
|
|
53
|
+
formatActivityStatus,
|
|
54
|
+
} from "../shared/activity-status.ts";
|
|
55
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
46
56
|
import { waitBounded } from "../shared/child-session.ts";
|
|
57
|
+
import { contextPercent } from "../shared/context-utilization.ts";
|
|
47
58
|
import {
|
|
48
59
|
registerEditorLayer,
|
|
49
60
|
removeEditorLayer,
|
|
50
61
|
} from "../shared/editor-layers.ts";
|
|
51
62
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
63
|
+
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
|
|
52
64
|
import {
|
|
53
65
|
OPENPI_TOOL_SURFACE,
|
|
54
66
|
patchOwnedTools,
|
|
55
67
|
} from "../shared/tool-surface.ts";
|
|
56
68
|
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} from "../
|
|
69
|
+
notifyWebCapabilities,
|
|
70
|
+
projectWorkflowCapability,
|
|
71
|
+
registerWebCapability,
|
|
72
|
+
type WebCapabilityScope,
|
|
73
|
+
} from "../shared/web-observer-registry.ts";
|
|
62
74
|
import {
|
|
63
75
|
createWorktree,
|
|
64
76
|
reclaimWorktree,
|
|
65
77
|
type Worktree,
|
|
66
78
|
type WorktreeCleanup,
|
|
67
79
|
} from "../shared/worktree.ts";
|
|
80
|
+
import {
|
|
81
|
+
loadAgentTypes,
|
|
82
|
+
resolveAgentModel,
|
|
83
|
+
roleModelForAgentType,
|
|
84
|
+
selectSubagentModel,
|
|
85
|
+
} from "../subagents/src/agent-types.ts";
|
|
86
|
+
import {
|
|
87
|
+
acceptanceInstruction,
|
|
88
|
+
acceptanceSchema,
|
|
89
|
+
applyAcceptance,
|
|
90
|
+
parseAcceptanceContract,
|
|
91
|
+
} from "./acceptance.ts";
|
|
68
92
|
import {
|
|
69
93
|
createWorkflowPersistence,
|
|
70
94
|
loadJournal,
|
|
95
|
+
persistWorkflowAgentResult,
|
|
96
|
+
persistWorkflowDeliveryState,
|
|
71
97
|
persistWorkflowJson,
|
|
98
|
+
persistWorkflowTerminalState,
|
|
72
99
|
} from "./artifacts.ts";
|
|
100
|
+
import {
|
|
101
|
+
buildExpandedWorkflowCompletion,
|
|
102
|
+
buildWorkflowCompletionDisplay,
|
|
103
|
+
isWorkflowCompletionDisplay,
|
|
104
|
+
workflowCompletionAlerts,
|
|
105
|
+
workflowCompletionResultPreview,
|
|
106
|
+
workflowCompletionSummary,
|
|
107
|
+
} from "./completion-projection.ts";
|
|
108
|
+
import { RunController } from "./controller.ts";
|
|
109
|
+
import {
|
|
110
|
+
resolveWorkflowLaunchMode,
|
|
111
|
+
waitForWorkflowCompletion,
|
|
112
|
+
} from "./coordinator.ts";
|
|
113
|
+
import {
|
|
114
|
+
listPersistedRunIds,
|
|
115
|
+
readPersistedWorkflowDetails,
|
|
116
|
+
recoverStaleWorkflowDetails,
|
|
117
|
+
sessionWorkflowRunIds,
|
|
118
|
+
showWorkflowDashboard,
|
|
119
|
+
} from "./dashboard.ts";
|
|
73
120
|
import { createWorkflowHandoffRegistry } from "./handoff.ts";
|
|
74
121
|
import {
|
|
75
122
|
classifyInterruptedInvocation,
|
|
@@ -77,57 +124,60 @@ import {
|
|
|
77
124
|
requestInvocation,
|
|
78
125
|
transitionInvocation,
|
|
79
126
|
} from "./invocation-ledger.ts";
|
|
80
|
-
import {
|
|
81
|
-
normalizeWorkflowOperatorKey,
|
|
82
|
-
WorkflowOperatorRegistry,
|
|
83
|
-
} from "./operator.ts";
|
|
84
127
|
import {
|
|
85
128
|
agentCallKey,
|
|
129
|
+
createJournalAccumulator,
|
|
86
130
|
createReplayCache,
|
|
87
|
-
type JournalEntry,
|
|
88
131
|
type ReplayCache,
|
|
89
132
|
} from "./journal.ts";
|
|
90
|
-
import { RunController } from "./controller.ts";
|
|
91
|
-
import {
|
|
92
|
-
normalizePersistedWorkflowDetails,
|
|
93
|
-
recoverStaleWorkflowDetails,
|
|
94
|
-
sessionWorkflowRunIds,
|
|
95
|
-
showWorkflowDashboard,
|
|
96
|
-
} from "./dashboard.ts";
|
|
97
133
|
import {
|
|
98
134
|
extractMeta,
|
|
99
135
|
prepareWorkflowScript,
|
|
100
136
|
type WorkflowMeta,
|
|
101
137
|
} from "./meta.ts";
|
|
102
138
|
import {
|
|
139
|
+
type AgentRecord,
|
|
103
140
|
agentContext,
|
|
104
141
|
aggregateUsage,
|
|
105
142
|
appendLog,
|
|
143
|
+
compactWorkflowToolDetails,
|
|
106
144
|
countStates,
|
|
145
|
+
createUsageReader,
|
|
107
146
|
emptyUsage,
|
|
108
147
|
formatElapsed,
|
|
109
148
|
formatUsage,
|
|
110
149
|
isWorkflowRunId,
|
|
111
150
|
phaseGroups,
|
|
151
|
+
refreshWorkflowGraph,
|
|
112
152
|
resolveWorkflowRunTarget,
|
|
113
153
|
resultJson,
|
|
114
154
|
sanitizeLine,
|
|
115
155
|
sanitizeWorkflowDisplayLine,
|
|
116
156
|
sanitizeWorkflowDisplayText,
|
|
117
|
-
|
|
157
|
+
stateGlyph,
|
|
118
158
|
statusColor,
|
|
159
|
+
statusGlyph,
|
|
119
160
|
statusWord,
|
|
120
|
-
createUsageReader,
|
|
121
|
-
refreshWorkflowGraph,
|
|
122
|
-
SQUARE,
|
|
123
|
-
type AgentRecord,
|
|
124
161
|
type WorkflowDetails,
|
|
125
162
|
} from "./model.ts";
|
|
126
163
|
import {
|
|
127
|
-
|
|
164
|
+
WorkflowNavigationEditor,
|
|
165
|
+
type WorkflowStripEntry,
|
|
166
|
+
WorkflowStripState,
|
|
167
|
+
WorkflowStripWidget,
|
|
168
|
+
workflowStripEntryKey,
|
|
169
|
+
} from "./navigation.ts";
|
|
170
|
+
import {
|
|
171
|
+
normalizeWorkflowOperatorKey,
|
|
172
|
+
WorkflowOperatorRegistry,
|
|
173
|
+
} from "./operator.ts";
|
|
174
|
+
import {
|
|
128
175
|
buildBackgroundWorkflowLaunchResult,
|
|
176
|
+
buildProjectedWorkflowCompletionBatches,
|
|
177
|
+
buildProjectedWorkflowResultMessage,
|
|
129
178
|
buildWorkflowAgentPrompt,
|
|
130
179
|
buildWorkflowResultMessage,
|
|
180
|
+
buildWorkflowStatusSummary,
|
|
131
181
|
WORKFLOW_LIFECYCLE_PROMPT_SNIPPET,
|
|
132
182
|
WORKFLOW_PARAMETER_DESCRIPTIONS,
|
|
133
183
|
WORKFLOW_PROMPT_GUIDELINES,
|
|
@@ -139,39 +189,336 @@ import {
|
|
|
139
189
|
WORKFLOW_TOOL_DESCRIPTION,
|
|
140
190
|
} from "./prompt.ts";
|
|
141
191
|
import {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
beginProcessReplayWorkspaceLease,
|
|
193
|
+
createReplayIdentity,
|
|
194
|
+
isReplaySafeAgentCall,
|
|
195
|
+
} from "./replay-safety.ts";
|
|
196
|
+
import {
|
|
197
|
+
createWorkflowResultDelivery,
|
|
198
|
+
type WorkflowCompletionEnvelope,
|
|
199
|
+
} from "./result-delivery.ts";
|
|
200
|
+
import {
|
|
201
|
+
createWorkflowSettledRunRetention,
|
|
202
|
+
projectWorkflowDetails,
|
|
203
|
+
type WorkflowSettledRunRetentionOptions,
|
|
204
|
+
} from "./retention.ts";
|
|
147
205
|
import {
|
|
148
206
|
createWorkflowResources,
|
|
149
207
|
runAgent,
|
|
150
208
|
type ThinkingLevel,
|
|
209
|
+
type WorkflowAgentSessionFactory,
|
|
151
210
|
type WorkflowModel,
|
|
152
211
|
} from "./runner.ts";
|
|
153
|
-
import {
|
|
154
|
-
beginProcessReplayWorkspaceLease,
|
|
155
|
-
createReplayIdentity,
|
|
156
|
-
isReplaySafeAgentCall,
|
|
157
|
-
} from "./replay-safety.ts";
|
|
158
212
|
import { runWorkflowSandbox } from "./sandbox.ts";
|
|
159
|
-
import {
|
|
160
|
-
acceptanceInstruction,
|
|
161
|
-
acceptanceSchema,
|
|
162
|
-
applyAcceptance,
|
|
163
|
-
evaluateAcceptance,
|
|
164
|
-
parseAcceptanceContract,
|
|
165
|
-
} from "./acceptance.ts";
|
|
213
|
+
import { writeFileAtomic } from "./serialization.ts";
|
|
166
214
|
import {
|
|
167
215
|
finalizeWorktreeHandoff,
|
|
168
216
|
prepareWorktreeHandoff,
|
|
169
217
|
} from "./worktree-handoff.ts";
|
|
170
|
-
import { safeStringify, writeFileAtomic } from "./serialization.ts";
|
|
171
218
|
|
|
172
219
|
const PREVIEW_LENGTH = 200;
|
|
173
220
|
const EMIT_INTERVAL_MS = 120;
|
|
174
221
|
|
|
222
|
+
/** Header of a workflow card: identity and phase on the left, metrics right. */
|
|
223
|
+
function runHeader(
|
|
224
|
+
details: WorkflowDetails,
|
|
225
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
226
|
+
now: number,
|
|
227
|
+
) {
|
|
228
|
+
const { done, failed, uncertain } = countStates(details);
|
|
229
|
+
const settled = done + failed;
|
|
230
|
+
const elapsed = formatElapsed(details.startedAt, details.finishedAt, now);
|
|
231
|
+
// A just-launched run has no agents and a 0s clock; the metrics join in
|
|
232
|
+
// once there is something real to report.
|
|
233
|
+
const counts =
|
|
234
|
+
details.agents.length > 0
|
|
235
|
+
? `${settled}/${details.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""}`
|
|
236
|
+
: undefined;
|
|
237
|
+
const metrics = [counts, counts || elapsed !== "0s" ? elapsed : undefined]
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.join(" · ");
|
|
240
|
+
let left =
|
|
241
|
+
`${statusGlyph(details.status, theme, now)} ${theme.fg("toolTitle", theme.bold("workflow "))}` +
|
|
242
|
+
theme.fg(
|
|
243
|
+
"accent",
|
|
244
|
+
sanitizeWorkflowDisplayLine(details.name ?? details.runId),
|
|
245
|
+
);
|
|
246
|
+
if (details.status === "running" && details.currentPhase) {
|
|
247
|
+
left += theme.fg(
|
|
248
|
+
"muted",
|
|
249
|
+
` · ${sanitizeWorkflowDisplayLine(details.currentPhase)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
// The glyph already carries the run state, so the status word only stays
|
|
253
|
+
// for terminal states.
|
|
254
|
+
let right = theme.fg("dim", metrics);
|
|
255
|
+
if (details.status !== "running") {
|
|
256
|
+
right +=
|
|
257
|
+
theme.fg("dim", `${metrics ? " · " : ""}`) +
|
|
258
|
+
theme.fg(statusColor(details.status), statusWord(details.status));
|
|
259
|
+
}
|
|
260
|
+
if (failed) right += theme.fg("error", ` · ${failed} failed`);
|
|
261
|
+
return { left, right };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Collapsed workflow card, rebuilt per repaint. Metrics right-align like the
|
|
266
|
+
* below-editor strip, and each agent row carries one number only: context
|
|
267
|
+
* occupancy says a running agent is alive, elapsed says what a settled one
|
|
268
|
+
* cost. A swarm shows the first few agents and summarizes the rest instead
|
|
269
|
+
* of flooding the chat.
|
|
270
|
+
*/
|
|
271
|
+
function buildCollapsedRows(
|
|
272
|
+
details: WorkflowDetails,
|
|
273
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
274
|
+
width: number,
|
|
275
|
+
now: number,
|
|
276
|
+
totals: string,
|
|
277
|
+
) {
|
|
278
|
+
const header = runHeader(details, theme, now);
|
|
279
|
+
const rows = [fitNavigationSides(header.left, header.right, width)];
|
|
280
|
+
const collapsedAgents = details.agents.slice(0, 8);
|
|
281
|
+
for (const agent of collapsedAgents) {
|
|
282
|
+
const percent = contextPercent({
|
|
283
|
+
tokens: agent.usage.contextTokens,
|
|
284
|
+
contextWindow: agent.contextWindow,
|
|
285
|
+
});
|
|
286
|
+
const stat =
|
|
287
|
+
agent.state === "running"
|
|
288
|
+
? percent === undefined
|
|
289
|
+
? undefined
|
|
290
|
+
: `${percent}%`
|
|
291
|
+
: formatElapsed(agent.startedAt, agent.finishedAt, now);
|
|
292
|
+
const left = ` ${stateGlyph(agent.state, theme, now)} ${theme.fg(
|
|
293
|
+
"accent",
|
|
294
|
+
sanitizeWorkflowDisplayLine(agent.label),
|
|
295
|
+
)}`;
|
|
296
|
+
rows.push(
|
|
297
|
+
stat
|
|
298
|
+
? fitNavigationSides(left, theme.fg("dim", stat), width)
|
|
299
|
+
: truncateToWidth(left, width, "…"),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const hiddenAgents = details.agents.length - collapsedAgents.length;
|
|
303
|
+
if (hiddenAgents > 0) {
|
|
304
|
+
rows.push(` ${theme.fg("dim", `… ${hiddenAgents} more`)}`);
|
|
305
|
+
}
|
|
306
|
+
// Only the tail collapsed: the newest lines are the ones that say where
|
|
307
|
+
// the run is now.
|
|
308
|
+
for (const entry of (details.logs ?? []).slice(-3)) {
|
|
309
|
+
rows.push(
|
|
310
|
+
truncateToWidth(
|
|
311
|
+
` ${theme.fg("muted", "›")} ${theme.fg(
|
|
312
|
+
"dim",
|
|
313
|
+
sanitizeWorkflowDisplayLine(entry.text),
|
|
314
|
+
)}`,
|
|
315
|
+
width,
|
|
316
|
+
"…",
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (totals) rows.push(` ${theme.fg("dim", `Total: ${totals}`)}`);
|
|
321
|
+
if (details.error) {
|
|
322
|
+
rows.push(
|
|
323
|
+
truncateToWidth(
|
|
324
|
+
` ${theme.fg(
|
|
325
|
+
"error",
|
|
326
|
+
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
327
|
+
)}`,
|
|
328
|
+
width,
|
|
329
|
+
"…",
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
// Expanding only earns its hint when there is more to see.
|
|
334
|
+
if (
|
|
335
|
+
details.agents.length > 0 ||
|
|
336
|
+
(details.logs ?? []).length > 0 ||
|
|
337
|
+
details.description ||
|
|
338
|
+
details.result !== undefined ||
|
|
339
|
+
details.error
|
|
340
|
+
) {
|
|
341
|
+
rows.push(
|
|
342
|
+
theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return rows;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildExpandedWorkflow(
|
|
349
|
+
details: WorkflowDetails,
|
|
350
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
351
|
+
now: number,
|
|
352
|
+
totals: string,
|
|
353
|
+
) {
|
|
354
|
+
const header = runHeader(details, theme, now);
|
|
355
|
+
const container = new Container();
|
|
356
|
+
container.addChild(
|
|
357
|
+
new Text(
|
|
358
|
+
header.right ? `${header.left} ${header.right}` : header.left,
|
|
359
|
+
0,
|
|
360
|
+
0,
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
if (details.description) {
|
|
364
|
+
container.addChild(
|
|
365
|
+
new Text(
|
|
366
|
+
theme.fg("dim", sanitizeWorkflowDisplayLine(details.description)),
|
|
367
|
+
0,
|
|
368
|
+
0,
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const group of phaseGroups(details)) {
|
|
374
|
+
container.addChild(new Spacer(1));
|
|
375
|
+
container.addChild(
|
|
376
|
+
new Text(
|
|
377
|
+
theme.fg(
|
|
378
|
+
"muted",
|
|
379
|
+
`─── ${sanitizeWorkflowDisplayLine(group.title)} ───`,
|
|
380
|
+
),
|
|
381
|
+
0,
|
|
382
|
+
0,
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
for (const agent of group.agents) {
|
|
386
|
+
const usage = formatUsage(agent.usage, agent.model);
|
|
387
|
+
const context = agentContext(agent);
|
|
388
|
+
let line = `${stateGlyph(agent.state, theme, now)} ${theme.fg(
|
|
389
|
+
"accent",
|
|
390
|
+
sanitizeWorkflowDisplayLine(agent.label),
|
|
391
|
+
)} ${theme.fg(
|
|
392
|
+
"dim",
|
|
393
|
+
[context, formatElapsed(agent.startedAt, agent.finishedAt, now)]
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join(" · "),
|
|
396
|
+
)}`;
|
|
397
|
+
if (usage)
|
|
398
|
+
line += ` ${theme.fg("dim", sanitizeWorkflowDisplayLine(usage))}`;
|
|
399
|
+
container.addChild(new Text(line, 0, 0));
|
|
400
|
+
if (agent.error) {
|
|
401
|
+
container.addChild(
|
|
402
|
+
new Text(
|
|
403
|
+
` ${theme.fg("error", sanitizeWorkflowDisplayLine(agent.error))}`,
|
|
404
|
+
0,
|
|
405
|
+
0,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
} else if (agent.preview) {
|
|
409
|
+
const preview = sanitizeWorkflowDisplayText(
|
|
410
|
+
agent.preview,
|
|
411
|
+
PREVIEW_LENGTH,
|
|
412
|
+
)
|
|
413
|
+
.split("\n")
|
|
414
|
+
.slice(0, 2)
|
|
415
|
+
.join(" ");
|
|
416
|
+
container.addChild(new Text(` ${theme.fg("dim", preview)}`, 0, 0));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (details.logs && details.logs.length > 0) {
|
|
422
|
+
container.addChild(new Spacer(1));
|
|
423
|
+
container.addChild(new Text(theme.fg("muted", "─── log ───"), 0, 0));
|
|
424
|
+
if (details.logsDropped) {
|
|
425
|
+
container.addChild(
|
|
426
|
+
new Text(
|
|
427
|
+
theme.fg("dim", `(${details.logsDropped} earlier line(s) dropped)`),
|
|
428
|
+
0,
|
|
429
|
+
0,
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
for (const entry of details.logs) {
|
|
434
|
+
container.addChild(
|
|
435
|
+
new Text(
|
|
436
|
+
`${theme.fg("muted", "›")} ${theme.fg(
|
|
437
|
+
"dim",
|
|
438
|
+
sanitizeWorkflowDisplayLine(entry.text),
|
|
439
|
+
)}`,
|
|
440
|
+
0,
|
|
441
|
+
0,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (details.error) {
|
|
448
|
+
container.addChild(new Spacer(1));
|
|
449
|
+
container.addChild(
|
|
450
|
+
new Text(
|
|
451
|
+
theme.fg(
|
|
452
|
+
"error",
|
|
453
|
+
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
454
|
+
),
|
|
455
|
+
0,
|
|
456
|
+
0,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (details.result !== undefined) {
|
|
462
|
+
container.addChild(new Spacer(1));
|
|
463
|
+
container.addChild(new Text(theme.fg("muted", "─── result ───"), 0, 0));
|
|
464
|
+
container.addChild(
|
|
465
|
+
new Markdown(
|
|
466
|
+
`\`\`\`json\n${resultJson(details.result)}\n\`\`\``,
|
|
467
|
+
0,
|
|
468
|
+
0,
|
|
469
|
+
getMarkdownTheme(),
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (totals) {
|
|
475
|
+
container.addChild(new Spacer(1));
|
|
476
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${totals}`), 0, 0));
|
|
477
|
+
}
|
|
478
|
+
return container;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
interface WorkflowRenderState {
|
|
482
|
+
spinnerTimer?: ReturnType<typeof setInterval>;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function syncWorkflowSpinner(
|
|
486
|
+
state: WorkflowRenderState,
|
|
487
|
+
isRunning: () => boolean,
|
|
488
|
+
invalidate: () => void,
|
|
489
|
+
) {
|
|
490
|
+
if (!isRunning()) {
|
|
491
|
+
if (state.spinnerTimer) clearInterval(state.spinnerTimer);
|
|
492
|
+
state.spinnerTimer = undefined;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (state.spinnerTimer) return;
|
|
496
|
+
const spinnerTimer = setInterval(() => {
|
|
497
|
+
if (state.spinnerTimer !== spinnerTimer) return;
|
|
498
|
+
if (!isRunning()) {
|
|
499
|
+
clearInterval(spinnerTimer);
|
|
500
|
+
state.spinnerTimer = undefined;
|
|
501
|
+
}
|
|
502
|
+
invalidate();
|
|
503
|
+
}, SPINNER_INTERVAL_MS);
|
|
504
|
+
state.spinnerTimer = spinnerTimer;
|
|
505
|
+
spinnerTimer.unref?.();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Test-only injection seam for execute-level tests: production never sets it.
|
|
510
|
+
* The underscore-prefixed setter name makes any accidental production use
|
|
511
|
+
* self-evidently wrong.
|
|
512
|
+
*/
|
|
513
|
+
let testAgentSessionFactory: WorkflowAgentSessionFactory | undefined;
|
|
514
|
+
|
|
515
|
+
/** Test-only: override how workflow children create their agent sessions. */
|
|
516
|
+
export function __setWorkflowTestAgentSessionFactory(
|
|
517
|
+
factory: WorkflowAgentSessionFactory | undefined,
|
|
518
|
+
) {
|
|
519
|
+
testAgentSessionFactory = factory;
|
|
520
|
+
}
|
|
521
|
+
|
|
175
522
|
const THINKING_LEVELS = [
|
|
176
523
|
"off",
|
|
177
524
|
"minimal",
|
|
@@ -209,26 +556,35 @@ interface AgentCallOptions {
|
|
|
209
556
|
inputs?: unknown;
|
|
210
557
|
}
|
|
211
558
|
|
|
212
|
-
const WorkflowParams = Type.Object(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
args: Type.Optional(
|
|
217
|
-
Type.String({
|
|
218
|
-
description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
|
|
219
|
-
}),
|
|
220
|
-
),
|
|
221
|
-
background: Type.Optional(
|
|
222
|
-
Type.Boolean({
|
|
223
|
-
description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
|
|
559
|
+
const WorkflowParams = Type.Object(
|
|
560
|
+
{
|
|
561
|
+
script: Type.String({
|
|
562
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.script,
|
|
224
563
|
}),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
564
|
+
args: Type.Optional(
|
|
565
|
+
Type.String({
|
|
566
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
|
|
567
|
+
}),
|
|
568
|
+
),
|
|
569
|
+
background: Type.Optional(
|
|
570
|
+
Type.Boolean({
|
|
571
|
+
deprecated: true,
|
|
572
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
|
|
573
|
+
}),
|
|
574
|
+
),
|
|
575
|
+
wait: Type.Optional(
|
|
576
|
+
Type.Boolean({
|
|
577
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
580
|
+
resume_from_run_id: Type.Optional(
|
|
581
|
+
Type.String({
|
|
582
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
|
|
583
|
+
}),
|
|
584
|
+
),
|
|
585
|
+
},
|
|
586
|
+
{ additionalProperties: false },
|
|
587
|
+
);
|
|
232
588
|
|
|
233
589
|
type WorkflowInput = Static<typeof WorkflowParams>;
|
|
234
590
|
|
|
@@ -266,13 +622,32 @@ function errorText(error: unknown): string {
|
|
|
266
622
|
);
|
|
267
623
|
}
|
|
268
624
|
|
|
625
|
+
function isWorkflowRenderDetails(value: unknown): value is WorkflowDetails {
|
|
626
|
+
if (!value || typeof value !== "object") return false;
|
|
627
|
+
const details = value as Partial<WorkflowDetails>;
|
|
628
|
+
return (
|
|
629
|
+
typeof details.runId === "string" &&
|
|
630
|
+
details.runId.length > 0 &&
|
|
631
|
+
typeof details.background === "boolean" &&
|
|
632
|
+
(details.status === "running" ||
|
|
633
|
+
details.status === "completed" ||
|
|
634
|
+
details.status === "failed" ||
|
|
635
|
+
details.status === "aborted" ||
|
|
636
|
+
details.status === "uncertain") &&
|
|
637
|
+
typeof details.startedAt === "number" &&
|
|
638
|
+
Number.isFinite(details.startedAt) &&
|
|
639
|
+
Array.isArray(details.phases) &&
|
|
640
|
+
Array.isArray(details.agents)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
269
644
|
function summaryLine(details: WorkflowDetails): string {
|
|
270
|
-
const { done, failed } = countStates(details);
|
|
645
|
+
const { done, failed, uncertain } = countStates(details);
|
|
271
646
|
const settled = done + failed;
|
|
272
647
|
// The newest narrator line beats the phase title when there is one: the
|
|
273
648
|
// script wrote it precisely because it says more than the phase does.
|
|
274
649
|
const latest = details.logs?.[details.logs.length - 1]?.text;
|
|
275
|
-
return `workflow ${details.name ?? details.runId}: ${settled}/${details.agents.length} agents${
|
|
650
|
+
return `workflow ${details.name ?? details.runId}: ${settled}/${details.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""}${
|
|
276
651
|
latest
|
|
277
652
|
? ` · ${latest}`
|
|
278
653
|
: details.currentPhase
|
|
@@ -285,20 +660,16 @@ function writeRunFile(runDir: string, name: string, content: string) {
|
|
|
285
660
|
writeFileAtomic(path.join(runDir, name), content);
|
|
286
661
|
}
|
|
287
662
|
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
: {}),
|
|
298
|
-
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
299
|
-
};
|
|
663
|
+
function appendArtifactPersistenceFailure(
|
|
664
|
+
details: WorkflowDetails,
|
|
665
|
+
error: unknown,
|
|
666
|
+
) {
|
|
667
|
+
const persistenceFailure = `Artifact persistence failed: ${errorText(error)}`;
|
|
668
|
+
if (details.status !== "aborted") details.status = "failed";
|
|
669
|
+
details.error = details.error
|
|
670
|
+
? `${details.error}; ${persistenceFailure}`
|
|
671
|
+
: persistenceFailure;
|
|
300
672
|
}
|
|
301
|
-
|
|
302
673
|
export interface ActiveWorkflowRunLifecycle {
|
|
303
674
|
details: WorkflowDetails;
|
|
304
675
|
controller: Pick<RunController, "abort" | "settle">;
|
|
@@ -306,6 +677,21 @@ export interface ActiveWorkflowRunLifecycle {
|
|
|
306
677
|
forceSettle(error: string): void;
|
|
307
678
|
}
|
|
308
679
|
|
|
680
|
+
interface WorkflowLifecycleTestHooks {
|
|
681
|
+
readonly persistWorkflow?: typeof persistWorkflowJson;
|
|
682
|
+
readonly reclaimWorktree?: typeof reclaimWorktree;
|
|
683
|
+
readonly onRunStarted?: (run: ActiveWorkflowRunLifecycle) => void;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let workflowLifecycleTestHooks: WorkflowLifecycleTestHooks | undefined;
|
|
687
|
+
|
|
688
|
+
/** Test-only control for deterministic lifecycle race coverage. */
|
|
689
|
+
export function __setWorkflowTestLifecycleHooks(
|
|
690
|
+
hooks: WorkflowLifecycleTestHooks | undefined,
|
|
691
|
+
) {
|
|
692
|
+
workflowLifecycleTestHooks = hooks;
|
|
693
|
+
}
|
|
694
|
+
|
|
309
695
|
/** Abort every live child and bound the whole session-shutdown barrier once. */
|
|
310
696
|
export async function shutdownActiveWorkflowRuns(
|
|
311
697
|
runs: readonly ActiveWorkflowRunLifecycle[],
|
|
@@ -348,15 +734,8 @@ function listRuns(
|
|
|
348
734
|
referencedRunIds: ReadonlySet<string>,
|
|
349
735
|
startedSince = 0,
|
|
350
736
|
): RunSummary[] {
|
|
351
|
-
const base = path.join(getAgentDir(), "workflows");
|
|
352
|
-
let names: string[] = [];
|
|
353
|
-
try {
|
|
354
|
-
names = fs.readdirSync(base).filter(isWorkflowRunId);
|
|
355
|
-
} catch {
|
|
356
|
-
// No runs yet.
|
|
357
|
-
}
|
|
358
737
|
const summaries: RunSummary[] = [];
|
|
359
|
-
for (const runId of
|
|
738
|
+
for (const runId of listPersistedRunIds()) {
|
|
360
739
|
const live = activeRuns.get(runId);
|
|
361
740
|
if (live) {
|
|
362
741
|
const { done, failed } = countStates(live);
|
|
@@ -371,34 +750,30 @@ function listRuns(
|
|
|
371
750
|
});
|
|
372
751
|
continue;
|
|
373
752
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const agents = parsed.agents ?? [];
|
|
387
|
-
summaries.push({
|
|
388
|
-
runId,
|
|
389
|
-
name: parsed.name,
|
|
390
|
-
status:
|
|
391
|
-
parsed.status === "running"
|
|
392
|
-
? "aborted"
|
|
393
|
-
: (parsed.status ?? "unknown"),
|
|
394
|
-
done: agents.filter((agent) => agent.state !== "running").length,
|
|
395
|
-
total: agents.length,
|
|
396
|
-
startedAt: parsed.startedAt ?? 0,
|
|
397
|
-
active: false,
|
|
398
|
-
});
|
|
399
|
-
} catch {
|
|
400
|
-
// Ignore unreadable artifacts because their session cannot be verified.
|
|
753
|
+
// Same reader as the dashboard and workflow_status: old-format runs are
|
|
754
|
+
// normalized here too, so every surface reports one status per run.
|
|
755
|
+
const details = readPersistedWorkflowDetails(runId, {
|
|
756
|
+
hydrateArtifacts: false,
|
|
757
|
+
});
|
|
758
|
+
if (!details) continue;
|
|
759
|
+
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
760
|
+
if (
|
|
761
|
+
touchedAt < startedSince ||
|
|
762
|
+
(details.sessionId !== sessionId && !referencedRunIds.has(runId))
|
|
763
|
+
) {
|
|
764
|
+
continue;
|
|
401
765
|
}
|
|
766
|
+
recoverStaleWorkflowDetails(details);
|
|
767
|
+
const { done, failed } = countStates(details);
|
|
768
|
+
summaries.push({
|
|
769
|
+
runId,
|
|
770
|
+
name: details.name,
|
|
771
|
+
status: details.status,
|
|
772
|
+
done: done + failed,
|
|
773
|
+
total: details.agents.length,
|
|
774
|
+
startedAt: details.startedAt,
|
|
775
|
+
active: false,
|
|
776
|
+
});
|
|
402
777
|
}
|
|
403
778
|
return summaries.sort((a, b) => b.startedAt - a.startedAt);
|
|
404
779
|
}
|
|
@@ -410,33 +785,49 @@ function runDetailText(
|
|
|
410
785
|
const runDir = path.join(getAgentDir(), "workflows", run.runId);
|
|
411
786
|
const live = activeRuns.get(run.runId);
|
|
412
787
|
if (live) return buildWorkflowResultMessage(live, runDir);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return buildWorkflowResultMessage(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
788
|
+
const details = readPersistedWorkflowDetails(run.runId, {
|
|
789
|
+
hydrateArtifacts: true,
|
|
790
|
+
});
|
|
791
|
+
if (details)
|
|
792
|
+
return buildWorkflowResultMessage(
|
|
793
|
+
recoverStaleWorkflowDetails(details),
|
|
794
|
+
runDir,
|
|
795
|
+
);
|
|
796
|
+
return `Run ${run.runId} — ${run.status}`;
|
|
421
797
|
}
|
|
422
798
|
|
|
423
|
-
export
|
|
799
|
+
export interface WorkflowExtensionOptions {
|
|
800
|
+
/** Test/configuration seam for the settled session-memory projection. */
|
|
801
|
+
readonly settledRetention?: WorkflowSettledRunRetentionOptions;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const WORKFLOW_DELIVERY_DETAILS_MAX_BYTES = 128 * 1024;
|
|
805
|
+
|
|
806
|
+
export default function workflows(
|
|
807
|
+
pi: ExtensionAPI,
|
|
808
|
+
options: WorkflowExtensionOptions = {},
|
|
809
|
+
) {
|
|
424
810
|
/** Live background runs, for /workflows and shutdown cleanup. */
|
|
425
811
|
const activeRuns = new Map<string, ActiveWorkflowRunLifecycle>();
|
|
812
|
+
let unregisterWebCapability: (() => void) | undefined;
|
|
813
|
+
let webCapabilityScope: WebCapabilityScope | undefined;
|
|
426
814
|
const activeDetails = () =>
|
|
427
815
|
new Map(
|
|
428
816
|
[...activeRuns].map(([runId, run]) => [runId, run.details] as const),
|
|
429
817
|
);
|
|
430
|
-
const settledRuns =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
818
|
+
const settledRuns = createWorkflowSettledRunRetention(
|
|
819
|
+
options.settledRetention,
|
|
820
|
+
);
|
|
821
|
+
/** Disk remains canonical; retained projections cover transient read failures. */
|
|
822
|
+
const dashboardDetails = () => activeDetails();
|
|
823
|
+
const dashboardRetainedDetails = () =>
|
|
824
|
+
new Map<string, WorkflowDetails>(settledRuns.entriesArray());
|
|
825
|
+
const registerStableToolFamily = () =>
|
|
436
826
|
patchOwnedTools(pi, "workflows", {
|
|
437
|
-
enable: OPENPI_TOOL_SURFACE.workflows.
|
|
827
|
+
enable: OPENPI_TOOL_SURFACE.workflows.entry,
|
|
438
828
|
});
|
|
439
829
|
const stripState = new WorkflowStripState();
|
|
830
|
+
const statusWriter = createStatusWriter("workflows");
|
|
440
831
|
const widgetKey = "workflow-navigation";
|
|
441
832
|
|
|
442
833
|
/**
|
|
@@ -444,9 +835,76 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
444
835
|
* next explicit request acknowledges them.
|
|
445
836
|
*/
|
|
446
837
|
let lastContext: ExtensionContext | undefined;
|
|
838
|
+
const completionEnvelope = (
|
|
839
|
+
details: WorkflowDetails,
|
|
840
|
+
): WorkflowCompletionEnvelope => {
|
|
841
|
+
const deliveryId = details.delivery?.id;
|
|
842
|
+
if (!deliveryId) throw new Error("Workflow delivery identity is missing");
|
|
843
|
+
const projection = projectWorkflowDetails(
|
|
844
|
+
details,
|
|
845
|
+
WORKFLOW_DELIVERY_DETAILS_MAX_BYTES,
|
|
846
|
+
);
|
|
847
|
+
if (!projection) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Workflow ${details.runId} cannot create a bounded completion projection`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
deliveryId,
|
|
854
|
+
runId: details.runId,
|
|
855
|
+
details: projection,
|
|
856
|
+
};
|
|
857
|
+
};
|
|
858
|
+
const resultDelivery = createWorkflowResultDelivery({
|
|
859
|
+
isIdle: () => lastContext?.isIdle() ?? false,
|
|
860
|
+
persist: (details) => {
|
|
861
|
+
if (!details.delivery)
|
|
862
|
+
throw new Error("Workflow delivery identity is missing");
|
|
863
|
+
persistWorkflowDeliveryState(
|
|
864
|
+
path.join(getAgentDir(), "workflows", details.runId),
|
|
865
|
+
details.delivery,
|
|
866
|
+
);
|
|
867
|
+
},
|
|
868
|
+
deliver: async (envelopes, wake) => {
|
|
869
|
+
const hydrated = envelopes.map((envelope) => ({
|
|
870
|
+
...envelope,
|
|
871
|
+
details:
|
|
872
|
+
readPersistedWorkflowDetails(envelope.runId, {
|
|
873
|
+
hydrateArtifacts: true,
|
|
874
|
+
}) ?? envelope.details,
|
|
875
|
+
}));
|
|
876
|
+
const sourceEntries = hydrated.map((envelope) => ({
|
|
877
|
+
deliveryId: envelope.deliveryId,
|
|
878
|
+
details: envelope.details,
|
|
879
|
+
runDir: path.join(getAgentDir(), "workflows", envelope.runId),
|
|
880
|
+
}));
|
|
881
|
+
const batches = buildProjectedWorkflowCompletionBatches(
|
|
882
|
+
sourceEntries,
|
|
883
|
+
lastContext?.getContextUsage?.(),
|
|
884
|
+
);
|
|
885
|
+
for (const batch of batches) {
|
|
886
|
+
pi.sendMessage(
|
|
887
|
+
{
|
|
888
|
+
customType: "workflow-result",
|
|
889
|
+
content: batch.content,
|
|
890
|
+
display: true,
|
|
891
|
+
details: buildWorkflowCompletionDisplay(batch.entries),
|
|
892
|
+
},
|
|
893
|
+
wake
|
|
894
|
+
? { deliverAs: "followUp", triggerTurn: true }
|
|
895
|
+
: { deliverAs: "nextTurn" },
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return envelopes.map((envelope) => ({
|
|
899
|
+
deliveryId: envelope.deliveryId,
|
|
900
|
+
delivered: true,
|
|
901
|
+
}));
|
|
902
|
+
},
|
|
903
|
+
});
|
|
447
904
|
let completedRuns = 0;
|
|
448
905
|
let failedRuns = 0;
|
|
449
906
|
let widgetVisible = false;
|
|
907
|
+
let widgetEntryKey: string | undefined;
|
|
450
908
|
let requestWidgetRender: (() => void) | undefined;
|
|
451
909
|
let navigationLayerRegistered = false;
|
|
452
910
|
let dashboardOpen = false;
|
|
@@ -477,16 +935,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
477
935
|
const running = newestEntry(
|
|
478
936
|
[...activeRuns].map(([runId, run]) => [runId, run.details] as const),
|
|
479
937
|
);
|
|
480
|
-
return running ?? newestEntry(settledRuns);
|
|
938
|
+
return running ?? newestEntry(settledRuns.entriesArray());
|
|
481
939
|
};
|
|
482
940
|
|
|
483
941
|
const updateWorkflowWidget = () => {
|
|
484
942
|
const ctx = lastContext;
|
|
485
943
|
if (!ctx || ctx.mode !== "tui") return;
|
|
486
|
-
const
|
|
487
|
-
|
|
944
|
+
const entry = stripEntry();
|
|
945
|
+
const visible = Boolean(entry);
|
|
946
|
+
const entryKey = workflowStripEntryKey(entry);
|
|
947
|
+
if (visible === widgetVisible) {
|
|
948
|
+
if (visible && entryKey !== widgetEntryKey) {
|
|
949
|
+
widgetEntryKey = entryKey;
|
|
950
|
+
requestWidgetRender?.();
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
488
954
|
if (!visible) {
|
|
489
955
|
stripState.focused = false;
|
|
956
|
+
widgetEntryKey = undefined;
|
|
490
957
|
requestWidgetRender = undefined;
|
|
491
958
|
ctx.ui.setWidget(widgetKey, undefined);
|
|
492
959
|
widgetVisible = false;
|
|
@@ -501,25 +968,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
501
968
|
{ placement: "belowEditor" },
|
|
502
969
|
);
|
|
503
970
|
widgetVisible = true;
|
|
971
|
+
widgetEntryKey = entryKey;
|
|
504
972
|
};
|
|
505
973
|
|
|
506
974
|
const updateIndicator = () => {
|
|
975
|
+
if (webCapabilityScope) notifyWebCapabilities(webCapabilityScope);
|
|
507
976
|
const ctx = lastContext;
|
|
508
977
|
if (!ctx) return;
|
|
509
978
|
try {
|
|
510
979
|
const running = activeRuns.size;
|
|
511
|
-
|
|
512
|
-
ctx.ui
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
"workflows",
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
);
|
|
522
|
-
}
|
|
980
|
+
statusWriter.write(
|
|
981
|
+
ctx.ui,
|
|
982
|
+
running === 0 && completedRuns === 0 && failedRuns === 0
|
|
983
|
+
? undefined
|
|
984
|
+
: formatActivityStatus(ctx.ui.theme, "workflows", {
|
|
985
|
+
running,
|
|
986
|
+
done: completedRuns,
|
|
987
|
+
failed: failedRuns,
|
|
988
|
+
}),
|
|
989
|
+
);
|
|
523
990
|
updateWorkflowWidget();
|
|
524
991
|
} catch {
|
|
525
992
|
// UI may be unavailable.
|
|
@@ -533,7 +1000,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
533
1000
|
};
|
|
534
1001
|
|
|
535
1002
|
const recordSettledRun = (details: WorkflowDetails) => {
|
|
536
|
-
settledRuns.set(details
|
|
1003
|
+
settledRuns.set(details);
|
|
537
1004
|
if (details.status === "completed") completedRuns += 1;
|
|
538
1005
|
else failedRuns += 1;
|
|
539
1006
|
};
|
|
@@ -556,10 +1023,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
556
1023
|
try {
|
|
557
1024
|
await showWorkflowDashboard(
|
|
558
1025
|
ctx,
|
|
559
|
-
|
|
1026
|
+
dashboardDetails,
|
|
560
1027
|
initialRunId,
|
|
561
1028
|
startedSince,
|
|
562
1029
|
stopRun,
|
|
1030
|
+
dashboardRetainedDetails,
|
|
563
1031
|
);
|
|
564
1032
|
acknowledgeSettledRuns();
|
|
565
1033
|
} finally {
|
|
@@ -593,7 +1061,21 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
593
1061
|
};
|
|
594
1062
|
|
|
595
1063
|
pi.on("session_start", (_event, ctx) => {
|
|
596
|
-
|
|
1064
|
+
unregisterWebCapability?.();
|
|
1065
|
+
const scope = ctx.sessionManager;
|
|
1066
|
+
webCapabilityScope = scope;
|
|
1067
|
+
unregisterWebCapability = registerWebCapability(scope, {
|
|
1068
|
+
kind: "workflows",
|
|
1069
|
+
snapshot: () =>
|
|
1070
|
+
projectWorkflowCapability([
|
|
1071
|
+
...activeDetails().values(),
|
|
1072
|
+
...settledRuns
|
|
1073
|
+
.entriesArray()
|
|
1074
|
+
.filter(([runId]) => !activeRuns.has(runId))
|
|
1075
|
+
.map(([, details]) => details),
|
|
1076
|
+
]),
|
|
1077
|
+
});
|
|
1078
|
+
registerStableToolFamily();
|
|
597
1079
|
if (ctx.hasUI) lastContext = ctx;
|
|
598
1080
|
agentTypes = loadAgentTypes({
|
|
599
1081
|
agentDir: getAgentDir(),
|
|
@@ -603,9 +1085,38 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
603
1085
|
turnStartedAt = 0;
|
|
604
1086
|
completedRuns = 0;
|
|
605
1087
|
failedRuns = 0;
|
|
606
|
-
settledRuns.
|
|
1088
|
+
settledRuns.resetSession();
|
|
607
1089
|
installWorkflowNavigation(ctx);
|
|
608
1090
|
updateIndicator();
|
|
1091
|
+
|
|
1092
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1093
|
+
for (const runId of listPersistedRunIds()) {
|
|
1094
|
+
const details = readPersistedWorkflowDetails(runId, {
|
|
1095
|
+
hydrateArtifacts: true,
|
|
1096
|
+
});
|
|
1097
|
+
if (!details || details.sessionId !== sessionId) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
const wasRunning = details.status === "running";
|
|
1101
|
+
if (wasRunning) {
|
|
1102
|
+
recoverStaleWorkflowDetails(details);
|
|
1103
|
+
persistWorkflowJson(
|
|
1104
|
+
path.join(getAgentDir(), "workflows", details.runId),
|
|
1105
|
+
details,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
// Pre-V2 terminal runs have no delivery receipt. They may already have
|
|
1109
|
+
// been shown, so replaying them on upgrade would be a surprising
|
|
1110
|
+
// duplicate. Only migrate owner-lost runs, whose uncertainty matters.
|
|
1111
|
+
if (details.delivery) {
|
|
1112
|
+
resultDelivery.restore(completionEnvelope(details));
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
void resultDelivery.flushIfIdle();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
pi.on("agent_settled", () => {
|
|
1119
|
+
void resultDelivery.parentSettled();
|
|
609
1120
|
});
|
|
610
1121
|
|
|
611
1122
|
pi.on("input", (event) => {
|
|
@@ -621,14 +1132,23 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
621
1132
|
navigationLayerRegistered = false;
|
|
622
1133
|
}
|
|
623
1134
|
await shutdownActiveWorkflowRuns([...activeRuns.values()]);
|
|
1135
|
+
// Give deferred completions one final delivery attempt. Failed sends stay
|
|
1136
|
+
// durably pending; clearing first would discard an envelope whose initial
|
|
1137
|
+
// persistence may have failed.
|
|
1138
|
+
await resultDelivery.parentSettled();
|
|
624
1139
|
try {
|
|
625
1140
|
lastContext?.ui.setStatus("workflows", undefined);
|
|
626
1141
|
lastContext?.ui.setWidget(widgetKey, undefined);
|
|
627
1142
|
} catch {
|
|
628
1143
|
// UI may already be disposed.
|
|
629
1144
|
}
|
|
1145
|
+
statusWriter.reset();
|
|
1146
|
+
unregisterWebCapability?.();
|
|
1147
|
+
unregisterWebCapability = undefined;
|
|
1148
|
+
webCapabilityScope = undefined;
|
|
630
1149
|
lastContext = undefined;
|
|
631
1150
|
widgetVisible = false;
|
|
1151
|
+
widgetEntryKey = undefined;
|
|
632
1152
|
requestWidgetRender = undefined;
|
|
633
1153
|
stripState.focused = false;
|
|
634
1154
|
});
|
|
@@ -736,7 +1256,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
736
1256
|
const meta = prepared.meta;
|
|
737
1257
|
const runId = `wf_${randomBytes(6).toString("hex")}`;
|
|
738
1258
|
const runDir = path.join(getAgentDir(), "workflows", runId);
|
|
739
|
-
const
|
|
1259
|
+
const canDeliverLater = ctx.hasUI && ctx.mode === "tui";
|
|
1260
|
+
const launchMode = resolveWorkflowLaunchMode(
|
|
1261
|
+
{ wait: params.wait, background: params.background },
|
|
1262
|
+
canDeliverLater,
|
|
1263
|
+
);
|
|
1264
|
+
const background = launchMode === "detached";
|
|
1265
|
+
const now = Date.now();
|
|
740
1266
|
|
|
741
1267
|
const details: WorkflowDetails = {
|
|
742
1268
|
runId,
|
|
@@ -745,15 +1271,21 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
745
1271
|
description: meta.description,
|
|
746
1272
|
background,
|
|
747
1273
|
status: "running",
|
|
748
|
-
startedAt:
|
|
1274
|
+
startedAt: now,
|
|
749
1275
|
phases: [...meta.phases],
|
|
750
1276
|
agents: [],
|
|
1277
|
+
delivery: {
|
|
1278
|
+
id: `workflow:${runId}:terminal`,
|
|
1279
|
+
state: launchMode === "inline" ? "held-for-inline" : "none",
|
|
1280
|
+
attempts: 0,
|
|
1281
|
+
updatedAt: now,
|
|
1282
|
+
},
|
|
751
1283
|
};
|
|
752
1284
|
|
|
753
1285
|
// Resume: replay cached results for calls whose content is unchanged.
|
|
754
1286
|
// A missing or unreadable source degrades to a normal full run — resume
|
|
755
1287
|
// is an optimization and must not become a new way to fail.
|
|
756
|
-
const
|
|
1288
|
+
const journal = createJournalAccumulator();
|
|
757
1289
|
let replay: ReplayCache | undefined;
|
|
758
1290
|
if (params.resume_from_run_id) {
|
|
759
1291
|
const source = resolveRunDir(params.resume_from_run_id);
|
|
@@ -773,16 +1305,19 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
773
1305
|
writeRunFile(runDir, "args.json", params.args);
|
|
774
1306
|
persistWorkflowJson(runDir, details);
|
|
775
1307
|
const persistence = createWorkflowPersistence(runDir, details, {
|
|
776
|
-
journal: () =>
|
|
1308
|
+
journal: () => journal,
|
|
1309
|
+
...(workflowLifecycleTestHooks?.persistWorkflow
|
|
1310
|
+
? { persist: workflowLifecycleTestHooks.persistWorkflow }
|
|
1311
|
+
: {}),
|
|
777
1312
|
});
|
|
778
1313
|
|
|
779
|
-
//
|
|
780
|
-
//
|
|
1314
|
+
// A caller wait never owns the run. All runs survive an interrupted
|
|
1315
|
+
// launch turn and are aborted only by workflow_stop/session shutdown.
|
|
781
1316
|
const workflowConfig = loadSetupConfig().workflows;
|
|
782
1317
|
const projectTrusted = ctx.isProjectTrusted();
|
|
783
1318
|
const runAgentTypes = agentTypes;
|
|
784
1319
|
const controller = new RunController(
|
|
785
|
-
|
|
1320
|
+
undefined,
|
|
786
1321
|
workflowConfig.concurrency,
|
|
787
1322
|
workflowConfig.maxAgentCalls,
|
|
788
1323
|
);
|
|
@@ -817,7 +1352,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
817
1352
|
if (background) return;
|
|
818
1353
|
onUpdate?.({
|
|
819
1354
|
content: [{ type: "text", text: summaryLine(details) }],
|
|
820
|
-
details:
|
|
1355
|
+
details: compactWorkflowToolDetails(details),
|
|
821
1356
|
});
|
|
822
1357
|
};
|
|
823
1358
|
const emit = (checkpoint = true) => {
|
|
@@ -835,6 +1370,15 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
835
1370
|
flush(terminal);
|
|
836
1371
|
};
|
|
837
1372
|
|
|
1373
|
+
const persistTerminalRecovery = () => {
|
|
1374
|
+
try {
|
|
1375
|
+
persistWorkflowTerminalState(runDir, details);
|
|
1376
|
+
} catch {
|
|
1377
|
+
// The original persistence error remains authoritative; restart
|
|
1378
|
+
// reconciliation handles the remaining uncertainty.
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
|
|
838
1382
|
const terminalize = (
|
|
839
1383
|
status: WorkflowDetails["status"],
|
|
840
1384
|
error?: string,
|
|
@@ -870,6 +1414,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
870
1414
|
}
|
|
871
1415
|
details.status = status;
|
|
872
1416
|
details.finishedAt = Date.now();
|
|
1417
|
+
if (details.delivery && background) {
|
|
1418
|
+
details.delivery = {
|
|
1419
|
+
...details.delivery,
|
|
1420
|
+
state: "pending",
|
|
1421
|
+
updatedAt: details.finishedAt,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
873
1424
|
refreshWorkflowGraph(details);
|
|
874
1425
|
if (error) details.error = sanitizeWorkflowDisplayLine(error);
|
|
875
1426
|
return true;
|
|
@@ -880,7 +1431,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
880
1431
|
try {
|
|
881
1432
|
persistence.flush();
|
|
882
1433
|
} catch (persistenceError) {
|
|
883
|
-
details
|
|
1434
|
+
appendArtifactPersistenceFailure(details, persistenceError);
|
|
1435
|
+
persistTerminalRecovery();
|
|
884
1436
|
}
|
|
885
1437
|
flushNow(true);
|
|
886
1438
|
};
|
|
@@ -897,15 +1449,19 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
897
1449
|
|
|
898
1450
|
// The script's narrator. Unlike phase(), this is append-only progress
|
|
899
1451
|
// text, so it never mutates the phase list a run is judged against.
|
|
900
|
-
const logFn = (text: string) => {
|
|
1452
|
+
const logFn = (text: string, kind?: "pipeline-drop") => {
|
|
901
1453
|
if (runSettled) return;
|
|
902
|
-
appendLog(details, text, Date.now());
|
|
1454
|
+
appendLog(details, text, Date.now(), kind);
|
|
903
1455
|
emit();
|
|
904
1456
|
};
|
|
905
1457
|
|
|
906
1458
|
// One reader per run: it carries a high-water mark, because per-agent
|
|
907
1459
|
// usage is recomputed from a message list that compaction shrinks.
|
|
908
|
-
const
|
|
1460
|
+
const readBaseUsage = createUsageReader(details.agents);
|
|
1461
|
+
const readUsage = () => ({
|
|
1462
|
+
...readBaseUsage(),
|
|
1463
|
+
limits: controller.capacity(),
|
|
1464
|
+
});
|
|
909
1465
|
|
|
910
1466
|
let agentCounter = 0;
|
|
911
1467
|
const agentFn = async (
|
|
@@ -1216,34 +1772,96 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1216
1772
|
});
|
|
1217
1773
|
const callKey = replayIdentity ? replayKey(replayIdentity) : undefined;
|
|
1218
1774
|
let replayBoundaryViolated = false;
|
|
1775
|
+
const persistAgentResult = (result: {
|
|
1776
|
+
output: string;
|
|
1777
|
+
structured?: unknown;
|
|
1778
|
+
}) => {
|
|
1779
|
+
try {
|
|
1780
|
+
return {
|
|
1781
|
+
ok: true as const,
|
|
1782
|
+
artifact: persistWorkflowAgentResult(runDir, index, result),
|
|
1783
|
+
};
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
return { ok: false as const, error: errorText(error) };
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1219
1788
|
// Checked before controller.schedule on purpose: schedule() charges the
|
|
1220
1789
|
// run's agent-call budget on entry, and a replayed call runs no agent.
|
|
1221
1790
|
const cached =
|
|
1222
1791
|
callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
|
|
1223
1792
|
if (cached) {
|
|
1224
1793
|
const finishedAt = Date.now();
|
|
1225
|
-
record.invocation = transitionInvocation(record.invocation!, {
|
|
1226
|
-
status: "replayed",
|
|
1227
|
-
at: finishedAt,
|
|
1228
|
-
});
|
|
1229
|
-
record.state = "done";
|
|
1230
|
-
record.replayed = true;
|
|
1231
1794
|
record.finishedAt = finishedAt;
|
|
1232
1795
|
record.preview = sanitizeWorkflowDisplayText(
|
|
1233
1796
|
cached.output,
|
|
1234
1797
|
PREVIEW_LENGTH,
|
|
1235
1798
|
);
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1799
|
+
const judged = applyAcceptance({
|
|
1800
|
+
contract: acceptanceContract,
|
|
1801
|
+
structured: cached.structured,
|
|
1802
|
+
agentOk: true,
|
|
1803
|
+
});
|
|
1804
|
+
if (judged.ledger) record.acceptance = judged.ledger;
|
|
1805
|
+
if (!judged.ok) {
|
|
1806
|
+
const error = sanitizeWorkflowDisplayLine(
|
|
1807
|
+
judged.error ?? "Agent failed",
|
|
1240
1808
|
);
|
|
1809
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1810
|
+
status: "rejected",
|
|
1811
|
+
at: finishedAt,
|
|
1812
|
+
});
|
|
1813
|
+
record.state = "error";
|
|
1814
|
+
record.error = error;
|
|
1815
|
+
emit();
|
|
1816
|
+
replayLease.end();
|
|
1817
|
+
return {
|
|
1818
|
+
ok: false,
|
|
1819
|
+
output: cached.output,
|
|
1820
|
+
...(cached.structured !== undefined
|
|
1821
|
+
? { structured: cached.structured }
|
|
1822
|
+
: {}),
|
|
1823
|
+
...(record.acceptance ? { acceptance: record.acceptance } : {}),
|
|
1824
|
+
error,
|
|
1825
|
+
};
|
|
1241
1826
|
}
|
|
1827
|
+
const persisted = persistAgentResult({
|
|
1828
|
+
output: cached.output,
|
|
1829
|
+
...(cached.structured !== undefined
|
|
1830
|
+
? { structured: cached.structured }
|
|
1831
|
+
: {}),
|
|
1832
|
+
});
|
|
1833
|
+
if (!persisted.ok) {
|
|
1834
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1835
|
+
status: "rejected",
|
|
1836
|
+
at: finishedAt,
|
|
1837
|
+
});
|
|
1838
|
+
record.state = "error";
|
|
1839
|
+
record.error = sanitizeWorkflowDisplayLine(persisted.error);
|
|
1840
|
+
emit();
|
|
1841
|
+
replayLease.end();
|
|
1842
|
+
return {
|
|
1843
|
+
ok: false,
|
|
1844
|
+
output: cached.output,
|
|
1845
|
+
...(cached.structured !== undefined
|
|
1846
|
+
? { structured: cached.structured }
|
|
1847
|
+
: {}),
|
|
1848
|
+
...(record.acceptance ? { acceptance: record.acceptance } : {}),
|
|
1849
|
+
error: persisted.error,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1853
|
+
status: "replayed",
|
|
1854
|
+
at: finishedAt,
|
|
1855
|
+
});
|
|
1856
|
+
record.state = "done";
|
|
1857
|
+
record.replayed = true;
|
|
1858
|
+
record.resultArtifact = persisted.artifact;
|
|
1242
1859
|
const ref = handoffs.register({
|
|
1243
1860
|
callId,
|
|
1244
1861
|
settled: true,
|
|
1245
1862
|
ok: true,
|
|
1246
1863
|
output: cached.output,
|
|
1864
|
+
resultArtifact: record.resultArtifact,
|
|
1247
1865
|
...(cached.structured !== undefined
|
|
1248
1866
|
? { structured: cached.structured }
|
|
1249
1867
|
: {}),
|
|
@@ -1252,7 +1870,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1252
1870
|
emit();
|
|
1253
1871
|
// Re-journal so a chain of resumes keeps working: run C resuming from
|
|
1254
1872
|
// B still finds what B replayed from A.
|
|
1255
|
-
|
|
1873
|
+
journal.append(cached);
|
|
1256
1874
|
replayLease.end();
|
|
1257
1875
|
return {
|
|
1258
1876
|
ok: true,
|
|
@@ -1365,6 +1983,9 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1365
1983
|
...(sessionManager ? { sessionManager } : {}),
|
|
1366
1984
|
modelRegistry: ctx.modelRegistry,
|
|
1367
1985
|
...(agentType?.tools ? { tools: agentType.tools } : {}),
|
|
1986
|
+
...(testAgentSessionFactory
|
|
1987
|
+
? { sessionFactory: testAgentSessionFactory }
|
|
1988
|
+
: {}),
|
|
1368
1989
|
...(replayIdentity
|
|
1369
1990
|
? {
|
|
1370
1991
|
replayFilesystemBoundary: {
|
|
@@ -1429,7 +2050,18 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1429
2050
|
});
|
|
1430
2051
|
const acceptance = judged.ledger;
|
|
1431
2052
|
if (acceptance) record.acceptance = acceptance;
|
|
1432
|
-
|
|
2053
|
+
let artifactError: string | undefined;
|
|
2054
|
+
if (judged.ok) {
|
|
2055
|
+
const persisted = persistAgentResult({
|
|
2056
|
+
output: outcome.output,
|
|
2057
|
+
...(outcome.structured !== undefined
|
|
2058
|
+
? { structured: outcome.structured }
|
|
2059
|
+
: {}),
|
|
2060
|
+
});
|
|
2061
|
+
if (persisted.ok) record.resultArtifact = persisted.artifact;
|
|
2062
|
+
else artifactError = persisted.error;
|
|
2063
|
+
}
|
|
2064
|
+
const outcomeOk = judged.ok && artifactError === undefined;
|
|
1433
2065
|
record.invocation = transitionInvocation(record.invocation!, {
|
|
1434
2066
|
status: "settled",
|
|
1435
2067
|
outcome: outcomeOk ? "success" : "error",
|
|
@@ -1437,15 +2069,20 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1437
2069
|
});
|
|
1438
2070
|
record.state = outcomeOk ? "done" : "error";
|
|
1439
2071
|
if (outcomeOk) delete record.error;
|
|
1440
|
-
else
|
|
1441
|
-
|
|
1442
|
-
|
|
2072
|
+
else {
|
|
2073
|
+
const failureError = artifactError ?? judged.error;
|
|
2074
|
+
record.error = failureError
|
|
2075
|
+
? sanitizeWorkflowDisplayLine(failureError)
|
|
1443
2076
|
: undefined;
|
|
2077
|
+
}
|
|
1444
2078
|
const ref = handoffs.register({
|
|
1445
2079
|
callId,
|
|
1446
2080
|
settled: true,
|
|
1447
2081
|
ok: outcomeOk,
|
|
1448
2082
|
output: outcome.output,
|
|
2083
|
+
...(record.resultArtifact
|
|
2084
|
+
? { resultArtifact: record.resultArtifact }
|
|
2085
|
+
: {}),
|
|
1449
2086
|
...(outcome.structured !== undefined
|
|
1450
2087
|
? { structured: outcome.structured }
|
|
1451
2088
|
: {}),
|
|
@@ -1477,7 +2114,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1477
2114
|
!replayBoundaryViolated &&
|
|
1478
2115
|
replayLease.canJournal()
|
|
1479
2116
|
) {
|
|
1480
|
-
|
|
2117
|
+
journal.append({
|
|
1481
2118
|
key: completedKey,
|
|
1482
2119
|
output: outcome.output,
|
|
1483
2120
|
...(outcome.structured !== undefined
|
|
@@ -1521,7 +2158,10 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1521
2158
|
detached: false,
|
|
1522
2159
|
};
|
|
1523
2160
|
} else {
|
|
1524
|
-
|
|
2161
|
+
const reclaimer =
|
|
2162
|
+
workflowLifecycleTestHooks?.reclaimWorktree ??
|
|
2163
|
+
reclaimWorktree;
|
|
2164
|
+
cleanup = await reclaimer(ctx.cwd, worktree).catch(
|
|
1525
2165
|
(error): WorktreeCleanup => ({
|
|
1526
2166
|
removed: false,
|
|
1527
2167
|
branchDeleted: false,
|
|
@@ -1543,13 +2183,21 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1543
2183
|
};
|
|
1544
2184
|
}
|
|
1545
2185
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2186
|
+
record.worktreeCleanup = cleanup;
|
|
2187
|
+
if (cleanup.branchDeleted) delete record.worktreeBranch;
|
|
2188
|
+
else record.worktreeBranch = cleanup.branch;
|
|
2189
|
+
if (!cleanup.removed) record.worktreePath = worktree.path;
|
|
2190
|
+
// Forced settlement fixes the execution verdict, but cleanup
|
|
2191
|
+
// provenance discovered afterward still belongs in the run.
|
|
2192
|
+
// No later final flush remains, so failures must be observable.
|
|
2193
|
+
if (runSettled) {
|
|
2194
|
+
try {
|
|
2195
|
+
persistence.flush();
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
appendArtifactPersistenceFailure(details, error);
|
|
2198
|
+
persistTerminalRecovery();
|
|
2199
|
+
}
|
|
2200
|
+
} else emit();
|
|
1553
2201
|
}
|
|
1554
2202
|
}
|
|
1555
2203
|
}, invocationSignal)
|
|
@@ -1603,8 +2251,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1603
2251
|
try {
|
|
1604
2252
|
persistence.flush();
|
|
1605
2253
|
} catch (error) {
|
|
1606
|
-
details
|
|
1607
|
-
|
|
2254
|
+
appendArtifactPersistenceFailure(details, error);
|
|
2255
|
+
persistTerminalRecovery();
|
|
1608
2256
|
throw new Error(details.error);
|
|
1609
2257
|
} finally {
|
|
1610
2258
|
flushNow(true);
|
|
@@ -1621,51 +2269,33 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1621
2269
|
activeRuns.set(runId, activeRun);
|
|
1622
2270
|
const completion = runScript();
|
|
1623
2271
|
activeRun.completion = completion;
|
|
2272
|
+
workflowLifecycleTestHooks?.onRunStarted?.(activeRun);
|
|
1624
2273
|
if (ctx.hasUI) lastContext = ctx;
|
|
1625
2274
|
updateIndicator();
|
|
1626
2275
|
|
|
2276
|
+
const recordTerminalRun = () => {
|
|
2277
|
+
activeRuns.delete(runId);
|
|
2278
|
+
recordSettledRun(details);
|
|
2279
|
+
updateIndicator();
|
|
2280
|
+
};
|
|
2281
|
+
|
|
2282
|
+
const settleForLaterDelivery = async (inlineReleased: boolean) => {
|
|
2283
|
+
try {
|
|
2284
|
+
await completion;
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
if (details.status === "running") details.status = "failed";
|
|
2287
|
+
details.finishedAt ??= Date.now();
|
|
2288
|
+
details.error = details.error ?? errorText(error);
|
|
2289
|
+
} finally {
|
|
2290
|
+
recordTerminalRun();
|
|
2291
|
+
const envelope = completionEnvelope(details);
|
|
2292
|
+
if (inlineReleased) resultDelivery.releaseInline(envelope);
|
|
2293
|
+
else resultDelivery.defer(envelope);
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
|
|
1627
2297
|
if (background) {
|
|
1628
|
-
void
|
|
1629
|
-
.catch((error) => {
|
|
1630
|
-
details.status = "failed";
|
|
1631
|
-
details.finishedAt = Date.now();
|
|
1632
|
-
details.error = details.error ?? errorText(error);
|
|
1633
|
-
})
|
|
1634
|
-
.finally(() => {
|
|
1635
|
-
activeRuns.delete(runId);
|
|
1636
|
-
recordSettledRun(details);
|
|
1637
|
-
updateIndicator();
|
|
1638
|
-
try {
|
|
1639
|
-
// Deliver like the subagent/terminal families: a custom-typed
|
|
1640
|
-
// session message with a dedicated renderer, not a plain
|
|
1641
|
-
// user-provenance turn.
|
|
1642
|
-
//
|
|
1643
|
-
// Wake the model only if it is idle and therefore plausibly
|
|
1644
|
-
// waiting on this run. If it is busy with something else, the
|
|
1645
|
-
// result still enters context with the user's next message
|
|
1646
|
-
// (nextTurn) instead of forcing a turn it can only acknowledge.
|
|
1647
|
-
const wake = ctx.isIdle();
|
|
1648
|
-
pi.sendMessage(
|
|
1649
|
-
{
|
|
1650
|
-
customType: "workflow-result",
|
|
1651
|
-
content: buildBackgroundWorkflowFollowUp({
|
|
1652
|
-
runId,
|
|
1653
|
-
name: details.name,
|
|
1654
|
-
status: details.status,
|
|
1655
|
-
result: buildWorkflowResultMessage(details, runDir),
|
|
1656
|
-
}),
|
|
1657
|
-
display: true,
|
|
1658
|
-
details: compactToolDetails(details),
|
|
1659
|
-
},
|
|
1660
|
-
wake
|
|
1661
|
-
? { deliverAs: "followUp", triggerTurn: true }
|
|
1662
|
-
: { deliverAs: "nextTurn" },
|
|
1663
|
-
);
|
|
1664
|
-
} catch {
|
|
1665
|
-
// Session may be shutting down.
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
showLifecycleTools();
|
|
2298
|
+
void settleForLaterDelivery(false);
|
|
1669
2299
|
return {
|
|
1670
2300
|
content: [
|
|
1671
2301
|
{
|
|
@@ -1677,17 +2307,19 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1677
2307
|
}),
|
|
1678
2308
|
},
|
|
1679
2309
|
],
|
|
1680
|
-
details:
|
|
2310
|
+
details: compactWorkflowToolDetails(details),
|
|
1681
2311
|
};
|
|
1682
2312
|
}
|
|
1683
2313
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2314
|
+
const waitOutcome = await waitForWorkflowCompletion(completion, signal);
|
|
2315
|
+
if (waitOutcome === "aborted") {
|
|
2316
|
+
void settleForLaterDelivery(true);
|
|
2317
|
+
throw new Error(
|
|
2318
|
+
`Workflow wait interrupted; run ${runId} continues in the background.`,
|
|
2319
|
+
);
|
|
1690
2320
|
}
|
|
2321
|
+
recordTerminalRun();
|
|
2322
|
+
resultDelivery.consumeInline(details);
|
|
1691
2323
|
if (details.status !== "completed") {
|
|
1692
2324
|
// Pi marks tool failures only when execute throws; returning isError is
|
|
1693
2325
|
// ignored by the extension API.
|
|
@@ -1697,10 +2329,14 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1697
2329
|
content: [
|
|
1698
2330
|
{
|
|
1699
2331
|
type: "text",
|
|
1700
|
-
text:
|
|
2332
|
+
text: buildProjectedWorkflowResultMessage(
|
|
2333
|
+
details,
|
|
2334
|
+
runDir,
|
|
2335
|
+
ctx.getContextUsage?.(),
|
|
2336
|
+
),
|
|
1701
2337
|
},
|
|
1702
2338
|
],
|
|
1703
|
-
details:
|
|
2339
|
+
details: compactWorkflowToolDetails(details),
|
|
1704
2340
|
};
|
|
1705
2341
|
},
|
|
1706
2342
|
|
|
@@ -1712,20 +2348,24 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1712
2348
|
let text =
|
|
1713
2349
|
theme.fg("toolTitle", theme.bold("workflow ")) +
|
|
1714
2350
|
theme.fg("accent", (meta as WorkflowMeta).name ?? "(script)");
|
|
1715
|
-
if (args.background
|
|
2351
|
+
if (args.background !== undefined) {
|
|
2352
|
+
text += theme.fg("dim", ` (deprecated: use wait: ${!args.background})`);
|
|
2353
|
+
} else if (args.wait === true) {
|
|
2354
|
+
text += theme.fg("dim", " (wait)");
|
|
2355
|
+
}
|
|
1716
2356
|
const description = (meta as WorkflowMeta).description;
|
|
1717
2357
|
if (description) text += `\n ${theme.fg("dim", description)}`;
|
|
1718
2358
|
for (const phase of meta.phases.slice(0, 8)) {
|
|
1719
|
-
text += `\n ${theme.fg("dim",
|
|
2359
|
+
text += `\n ${theme.fg("dim", "○")} ${theme.fg("accent", phase.title)}${
|
|
1720
2360
|
phase.detail ? theme.fg("dim", ` — ${phase.detail}`) : ""
|
|
1721
2361
|
}`;
|
|
1722
2362
|
}
|
|
1723
2363
|
return new Text(text, 0, 0);
|
|
1724
2364
|
},
|
|
1725
2365
|
|
|
1726
|
-
renderResult(result, { expanded }, theme) {
|
|
1727
|
-
const details = result.details
|
|
1728
|
-
if (!details) {
|
|
2366
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2367
|
+
const details = result.details;
|
|
2368
|
+
if (!isWorkflowRenderDetails(details)) {
|
|
1729
2369
|
const first = result.content[0];
|
|
1730
2370
|
return new Text(
|
|
1731
2371
|
first?.type === "text" ? first.text : "(no output)",
|
|
@@ -1733,225 +2373,53 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1733
2373
|
0,
|
|
1734
2374
|
);
|
|
1735
2375
|
}
|
|
2376
|
+
// A settled Pi tool result is committed transcript history. Keep its
|
|
2377
|
+
// launch snapshot stable; live run state belongs to the strip/dashboard.
|
|
2378
|
+
const settledAt = Date.now();
|
|
2379
|
+
const currentDetails = () =>
|
|
2380
|
+
isPartial
|
|
2381
|
+
? (activeRuns.get(details.runId)?.details ??
|
|
2382
|
+
settledRuns.get(details.runId) ??
|
|
2383
|
+
details)
|
|
2384
|
+
: details;
|
|
2385
|
+
syncWorkflowSpinner(
|
|
2386
|
+
context.state as WorkflowRenderState,
|
|
2387
|
+
() => isPartial && currentDetails().status === "running",
|
|
2388
|
+
context.invalidate,
|
|
2389
|
+
);
|
|
1736
2390
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
sanitizeWorkflowDisplayLine(details.name ?? details.runId),
|
|
1745
|
-
)} ` +
|
|
1746
|
-
theme.fg(
|
|
1747
|
-
"dim",
|
|
1748
|
-
`${settled}/${details.agents.length} agents · ${elapsed} · `,
|
|
1749
|
-
) +
|
|
1750
|
-
theme.fg(statusColor(details.status), statusWord(details.status));
|
|
1751
|
-
if (failed) header += theme.fg("error", ` · ${failed} failed`);
|
|
1752
|
-
if (details.background) header += theme.fg("dim", " (background)");
|
|
1753
|
-
if (details.status === "running" && details.currentPhase) {
|
|
1754
|
-
header += theme.fg(
|
|
1755
|
-
"muted",
|
|
1756
|
-
` · ${sanitizeWorkflowDisplayLine(details.currentPhase)}`,
|
|
1757
|
-
);
|
|
1758
|
-
}
|
|
1759
|
-
const totals = formatUsage(aggregateUsage(details.agents));
|
|
1760
|
-
|
|
1761
|
-
if (!expanded) {
|
|
1762
|
-
let text = header;
|
|
1763
|
-
for (const agent of details.agents) {
|
|
1764
|
-
const context = agentContext(agent);
|
|
1765
|
-
text += `\n ${stateSquare(agent.state, theme)} ${theme.fg(
|
|
1766
|
-
"accent",
|
|
1767
|
-
sanitizeWorkflowDisplayLine(agent.label),
|
|
1768
|
-
)}${
|
|
1769
|
-
agent.phase
|
|
1770
|
-
? theme.fg(
|
|
1771
|
-
"dim",
|
|
1772
|
-
` (${sanitizeWorkflowDisplayLine(agent.phase)})`,
|
|
1773
|
-
)
|
|
1774
|
-
: ""
|
|
1775
|
-
}${theme.fg(
|
|
1776
|
-
"dim",
|
|
1777
|
-
`${context ? ` · ${context}` : ""} · ${formatElapsed(agent.startedAt, agent.finishedAt)}`,
|
|
1778
|
-
)}`;
|
|
1779
|
-
}
|
|
1780
|
-
// Only the tail collapsed: the newest lines are the ones that say
|
|
1781
|
-
// where the run is now.
|
|
1782
|
-
for (const entry of (details.logs ?? []).slice(-3)) {
|
|
1783
|
-
text += `\n ${theme.fg("muted", "›")} ${theme.fg(
|
|
1784
|
-
"dim",
|
|
1785
|
-
sanitizeWorkflowDisplayLine(entry.text),
|
|
1786
|
-
)}`;
|
|
1787
|
-
}
|
|
1788
|
-
if (totals) text += `\n ${theme.fg("dim", `Total: ${totals}`)}`;
|
|
1789
|
-
if (details.error)
|
|
1790
|
-
text += `\n ${theme.fg(
|
|
1791
|
-
"error",
|
|
1792
|
-
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
1793
|
-
)}`;
|
|
1794
|
-
text += `\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
1795
|
-
return new Text(text, 0, 0);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
const container = new Container();
|
|
1799
|
-
container.addChild(new Text(header, 0, 0));
|
|
1800
|
-
if (details.description) {
|
|
1801
|
-
container.addChild(
|
|
1802
|
-
new Text(
|
|
1803
|
-
theme.fg("dim", sanitizeWorkflowDisplayLine(details.description)),
|
|
1804
|
-
0,
|
|
1805
|
-
0,
|
|
1806
|
-
),
|
|
1807
|
-
);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
for (const group of phaseGroups(details)) {
|
|
1811
|
-
container.addChild(new Spacer(1));
|
|
1812
|
-
container.addChild(
|
|
1813
|
-
new Text(
|
|
1814
|
-
theme.fg(
|
|
1815
|
-
"muted",
|
|
1816
|
-
`─── ${sanitizeWorkflowDisplayLine(group.title)} ───`,
|
|
1817
|
-
),
|
|
1818
|
-
0,
|
|
1819
|
-
0,
|
|
1820
|
-
),
|
|
1821
|
-
);
|
|
1822
|
-
for (const agent of group.agents) {
|
|
1823
|
-
const usage = formatUsage(agent.usage, agent.model);
|
|
1824
|
-
const context = agentContext(agent);
|
|
1825
|
-
let line = `${stateSquare(agent.state, theme)} ${theme.fg(
|
|
1826
|
-
"accent",
|
|
1827
|
-
sanitizeWorkflowDisplayLine(agent.label),
|
|
1828
|
-
)} ${theme.fg(
|
|
1829
|
-
"dim",
|
|
1830
|
-
[context, formatElapsed(agent.startedAt, agent.finishedAt)]
|
|
1831
|
-
.filter(Boolean)
|
|
1832
|
-
.join(" · "),
|
|
1833
|
-
)}`;
|
|
1834
|
-
if (usage)
|
|
1835
|
-
line += ` ${theme.fg("dim", sanitizeWorkflowDisplayLine(usage))}`;
|
|
1836
|
-
container.addChild(new Text(line, 0, 0));
|
|
1837
|
-
if (agent.error) {
|
|
1838
|
-
container.addChild(
|
|
1839
|
-
new Text(
|
|
1840
|
-
` ${theme.fg("error", sanitizeWorkflowDisplayLine(agent.error))}`,
|
|
1841
|
-
0,
|
|
1842
|
-
0,
|
|
1843
|
-
),
|
|
1844
|
-
);
|
|
1845
|
-
} else if (agent.preview) {
|
|
1846
|
-
const preview = sanitizeWorkflowDisplayText(
|
|
1847
|
-
agent.preview,
|
|
1848
|
-
PREVIEW_LENGTH,
|
|
1849
|
-
)
|
|
1850
|
-
.split("\n")
|
|
1851
|
-
.slice(0, 2)
|
|
1852
|
-
.join(" ");
|
|
1853
|
-
container.addChild(new Text(` ${theme.fg("dim", preview)}`, 0, 0));
|
|
2391
|
+
return {
|
|
2392
|
+
render(width: number) {
|
|
2393
|
+
const current = currentDetails();
|
|
2394
|
+
const totals = formatUsage(aggregateUsage(current.agents));
|
|
2395
|
+
const now = isPartial ? Date.now() : settledAt;
|
|
2396
|
+
if (!expanded) {
|
|
2397
|
+
return buildCollapsedRows(current, theme, width, now, totals);
|
|
1854
2398
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
if (details.logs && details.logs.length > 0) {
|
|
1859
|
-
container.addChild(new Spacer(1));
|
|
1860
|
-
container.addChild(new Text(theme.fg("muted", "─── log ───"), 0, 0));
|
|
1861
|
-
if (details.logsDropped) {
|
|
1862
|
-
container.addChild(
|
|
1863
|
-
new Text(
|
|
1864
|
-
theme.fg(
|
|
1865
|
-
"dim",
|
|
1866
|
-
`(${details.logsDropped} earlier line(s) dropped)`,
|
|
1867
|
-
),
|
|
1868
|
-
0,
|
|
1869
|
-
0,
|
|
1870
|
-
),
|
|
2399
|
+
return buildExpandedWorkflow(current, theme, now, totals).render(
|
|
2400
|
+
width,
|
|
1871
2401
|
);
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
new Text(
|
|
1876
|
-
`${theme.fg("muted", "›")} ${theme.fg(
|
|
1877
|
-
"dim",
|
|
1878
|
-
sanitizeWorkflowDisplayLine(entry.text),
|
|
1879
|
-
)}`,
|
|
1880
|
-
0,
|
|
1881
|
-
0,
|
|
1882
|
-
),
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
if (details.error) {
|
|
1888
|
-
container.addChild(new Spacer(1));
|
|
1889
|
-
container.addChild(
|
|
1890
|
-
new Text(
|
|
1891
|
-
theme.fg(
|
|
1892
|
-
"error",
|
|
1893
|
-
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
1894
|
-
),
|
|
1895
|
-
0,
|
|
1896
|
-
0,
|
|
1897
|
-
),
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
if (details.result !== undefined) {
|
|
1902
|
-
container.addChild(new Spacer(1));
|
|
1903
|
-
container.addChild(new Text(theme.fg("muted", "─── result ───"), 0, 0));
|
|
1904
|
-
container.addChild(
|
|
1905
|
-
new Markdown(
|
|
1906
|
-
`\`\`\`json\n${resultJson(details.result)}\n\`\`\``,
|
|
1907
|
-
0,
|
|
1908
|
-
0,
|
|
1909
|
-
getMarkdownTheme(),
|
|
1910
|
-
),
|
|
1911
|
-
);
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
if (totals) {
|
|
1915
|
-
container.addChild(new Spacer(1));
|
|
1916
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${totals}`), 0, 0));
|
|
1917
|
-
}
|
|
1918
|
-
return container;
|
|
2402
|
+
},
|
|
2403
|
+
invalidate() {},
|
|
2404
|
+
};
|
|
1919
2405
|
},
|
|
1920
2406
|
});
|
|
1921
2407
|
|
|
1922
2408
|
/** Resolve one run from live, settled, or persisted state. */
|
|
1923
2409
|
const resolveRunDetails = (target: string) => {
|
|
1924
|
-
const base = path.join(getAgentDir(), "workflows");
|
|
1925
|
-
let persistedIds: string[] = [];
|
|
1926
|
-
try {
|
|
1927
|
-
persistedIds = fs.readdirSync(base).filter(isWorkflowRunId);
|
|
1928
|
-
} catch {
|
|
1929
|
-
// In-memory runs remain inspectable without the artifact directory.
|
|
1930
|
-
}
|
|
1931
2410
|
const resolution = resolveWorkflowRunTarget(target, [
|
|
1932
2411
|
...activeRuns.keys(),
|
|
1933
2412
|
...settledRuns.keys(),
|
|
1934
|
-
...
|
|
2413
|
+
...listPersistedRunIds(),
|
|
1935
2414
|
]);
|
|
1936
2415
|
if (!resolution.ok) return resolution;
|
|
1937
2416
|
|
|
1938
2417
|
const active = activeRuns.get(resolution.runId);
|
|
1939
2418
|
if (active) return { ok: true, details: active.details } as const;
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
const parsed: unknown = JSON.parse(
|
|
1945
|
-
fs.readFileSync(
|
|
1946
|
-
path.join(base, resolution.runId, "workflow.json"),
|
|
1947
|
-
"utf8",
|
|
1948
|
-
),
|
|
1949
|
-
);
|
|
1950
|
-
const details = normalizePersistedWorkflowDetails(
|
|
1951
|
-
resolution.runId,
|
|
1952
|
-
parsed,
|
|
1953
|
-
);
|
|
1954
|
-
if (!details) throw new Error("invalid workflow details");
|
|
2419
|
+
const details = readPersistedWorkflowDetails(resolution.runId, {
|
|
2420
|
+
hydrateArtifacts: true,
|
|
2421
|
+
});
|
|
2422
|
+
if (details) {
|
|
1955
2423
|
// A run absent from activeRuns cannot still be running this session; a
|
|
1956
2424
|
// persisted "running" is a run that was hard-killed or missed the
|
|
1957
2425
|
// shutdown settle deadline.
|
|
@@ -1959,12 +2427,15 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1959
2427
|
ok: true,
|
|
1960
2428
|
details: recoverStaleWorkflowDetails(details),
|
|
1961
2429
|
} as const;
|
|
1962
|
-
} catch {
|
|
1963
|
-
return {
|
|
1964
|
-
ok: false,
|
|
1965
|
-
error: `Workflow run ${resolution.runId} could not be read.`,
|
|
1966
|
-
} as const;
|
|
1967
2430
|
}
|
|
2431
|
+
// Keep the bounded projection as a diagnostic fallback when an artifact is
|
|
2432
|
+
// temporarily unreadable. An explicit id still resolves to a known run.
|
|
2433
|
+
const settled = settledRuns.get(resolution.runId);
|
|
2434
|
+
if (settled) return { ok: true, details: settled } as const;
|
|
2435
|
+
return {
|
|
2436
|
+
ok: false,
|
|
2437
|
+
error: `Workflow run ${resolution.runId} could not be read.`,
|
|
2438
|
+
} as const;
|
|
1968
2439
|
};
|
|
1969
2440
|
|
|
1970
2441
|
pi.registerTool({
|
|
@@ -1978,23 +2449,29 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1978
2449
|
}),
|
|
1979
2450
|
}),
|
|
1980
2451
|
execute(_toolCallId, params) {
|
|
1981
|
-
const
|
|
1982
|
-
([, run]) => run.details.status === "running",
|
|
1983
|
-
);
|
|
1984
|
-
const resolution = resolveWorkflowRunTarget(
|
|
1985
|
-
params.runId,
|
|
1986
|
-
running.map(([runId]) => runId),
|
|
1987
|
-
);
|
|
2452
|
+
const resolution = resolveRunDetails(params.runId);
|
|
1988
2453
|
if (!resolution.ok) throw new Error(resolution.error);
|
|
1989
|
-
|
|
2454
|
+
const details = resolution.details;
|
|
2455
|
+
if (details.status !== "running") {
|
|
2456
|
+
return Promise.resolve({
|
|
2457
|
+
content: [
|
|
2458
|
+
{
|
|
2459
|
+
type: "text",
|
|
2460
|
+
text: `Workflow ${details.runId} is already ${details.status}.`,
|
|
2461
|
+
},
|
|
2462
|
+
],
|
|
2463
|
+
details: { runId: details.runId, status: details.status },
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
stopRun(details.runId);
|
|
1990
2467
|
return Promise.resolve({
|
|
1991
2468
|
content: [
|
|
1992
2469
|
{
|
|
1993
2470
|
type: "text",
|
|
1994
|
-
text: `Stopping workflow ${
|
|
2471
|
+
text: `Stopping workflow ${details.runId}.`,
|
|
1995
2472
|
},
|
|
1996
2473
|
],
|
|
1997
|
-
details: { runId:
|
|
2474
|
+
details: { runId: details.runId, status: "aborting" },
|
|
1998
2475
|
});
|
|
1999
2476
|
},
|
|
2000
2477
|
});
|
|
@@ -2014,13 +2491,14 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2014
2491
|
// Details are a uniform run-summary array (one entry for a single-id peek)
|
|
2015
2492
|
// so the tool has a single result shape; the text carries the detail.
|
|
2016
2493
|
const summarize = (d: WorkflowDetails) => {
|
|
2017
|
-
const { done, failed } = countStates(d);
|
|
2494
|
+
const { done, failed, uncertain } = countStates(d);
|
|
2018
2495
|
return {
|
|
2019
2496
|
runId: d.runId,
|
|
2020
2497
|
name: d.name,
|
|
2021
2498
|
status: d.status,
|
|
2022
2499
|
done,
|
|
2023
2500
|
failed,
|
|
2501
|
+
uncertain,
|
|
2024
2502
|
total: d.agents.length,
|
|
2025
2503
|
};
|
|
2026
2504
|
};
|
|
@@ -2029,32 +2507,57 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2029
2507
|
if (!resolution.ok) throw new Error(resolution.error);
|
|
2030
2508
|
const details = resolution.details;
|
|
2031
2509
|
const runDir = path.join(getAgentDir(), "workflows", details.runId);
|
|
2510
|
+
const retention = settledRuns.stats;
|
|
2032
2511
|
return Promise.resolve({
|
|
2033
2512
|
content: [
|
|
2034
|
-
{ type: "text", text:
|
|
2513
|
+
{ type: "text", text: buildWorkflowStatusSummary(details, runDir) },
|
|
2035
2514
|
],
|
|
2036
|
-
details: {
|
|
2515
|
+
details: {
|
|
2516
|
+
runs: [summarize(details)],
|
|
2517
|
+
retention,
|
|
2518
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2519
|
+
},
|
|
2037
2520
|
});
|
|
2038
2521
|
}
|
|
2039
2522
|
const runs = [
|
|
2040
2523
|
...[...activeRuns.values()].map((run) => run.details),
|
|
2041
2524
|
...settledRuns.values(),
|
|
2042
2525
|
];
|
|
2526
|
+
const retention = settledRuns.stats;
|
|
2043
2527
|
if (runs.length === 0) {
|
|
2044
2528
|
return Promise.resolve({
|
|
2045
2529
|
content: [
|
|
2046
|
-
{
|
|
2530
|
+
{
|
|
2531
|
+
type: "text",
|
|
2532
|
+
text:
|
|
2533
|
+
retention.evictedRuns > 0
|
|
2534
|
+
? `No active or retained workflows. ${retention.evictedRuns} settled run(s) omitted from memory in the current session; canonical artifacts remain available on disk.`
|
|
2535
|
+
: "No active or recently finished workflows.",
|
|
2536
|
+
},
|
|
2047
2537
|
],
|
|
2048
|
-
details: {
|
|
2538
|
+
details: {
|
|
2539
|
+
runs: [],
|
|
2540
|
+
retention,
|
|
2541
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2542
|
+
},
|
|
2049
2543
|
});
|
|
2050
2544
|
}
|
|
2051
2545
|
const lines = runs.map((d) => {
|
|
2052
|
-
const { done, failed } = countStates(d);
|
|
2053
|
-
return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}`;
|
|
2546
|
+
const { done, failed, uncertain } = countStates(d);
|
|
2547
|
+
return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`;
|
|
2054
2548
|
});
|
|
2549
|
+
if (retention.evictedRuns > 0) {
|
|
2550
|
+
lines.push(
|
|
2551
|
+
`Retention (current session): ${retention.retainedRuns} settled projection(s) retained; ${retention.evictedRuns} evicted/omitted (${retention.evictedBytes} UTF-8 bytes). Canonical artifacts remain available on disk.`,
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2055
2554
|
return Promise.resolve({
|
|
2056
2555
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
2057
|
-
details: {
|
|
2556
|
+
details: {
|
|
2557
|
+
runs: runs.map(summarize),
|
|
2558
|
+
retention,
|
|
2559
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2560
|
+
},
|
|
2058
2561
|
});
|
|
2059
2562
|
},
|
|
2060
2563
|
});
|
|
@@ -2062,7 +2565,6 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2062
2565
|
pi.registerMessageRenderer(
|
|
2063
2566
|
"workflow-result",
|
|
2064
2567
|
(message, { expanded }, theme) => {
|
|
2065
|
-
const details = message.details as WorkflowDetails | undefined;
|
|
2066
2568
|
const body =
|
|
2067
2569
|
typeof message.content === "string"
|
|
2068
2570
|
? message.content
|
|
@@ -2070,25 +2572,73 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2070
2572
|
?.map((part) => (part.type === "text" ? part.text : ""))
|
|
2071
2573
|
.join("") ?? "");
|
|
2072
2574
|
const safeBody = sanitizeWorkflowDisplayText(body);
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2575
|
+
const display = isWorkflowCompletionDisplay(message.details)
|
|
2576
|
+
? message.details
|
|
2577
|
+
: undefined;
|
|
2578
|
+
const legacyDetails = isWorkflowRenderDetails(message.details)
|
|
2579
|
+
? message.details
|
|
2580
|
+
: undefined;
|
|
2581
|
+
if (!display && !legacyDetails) {
|
|
2582
|
+
return new Text(safeBody, 0, 0);
|
|
2583
|
+
}
|
|
2584
|
+
if (legacyDetails) {
|
|
2585
|
+
const headerParts = runHeader(legacyDetails, theme, Date.now());
|
|
2586
|
+
const header = headerParts.right
|
|
2587
|
+
? `${headerParts.left} ${headerParts.right}`
|
|
2588
|
+
: headerParts.left;
|
|
2589
|
+
if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
|
|
2590
|
+
const preview = safeBody.split("\n").slice(0, 8).join("\n");
|
|
2591
|
+
return new Text(
|
|
2592
|
+
`${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
|
|
2593
|
+
0,
|
|
2594
|
+
0,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
if (!display) return new Text(safeBody, 0, 0);
|
|
2598
|
+
if (expanded) {
|
|
2599
|
+
return new Text(buildExpandedWorkflowCompletion(display), 0, 0);
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
render(width: number) {
|
|
2603
|
+
const rows: string[] = [];
|
|
2604
|
+
for (const entry of display.entries) {
|
|
2605
|
+
rows.push(
|
|
2606
|
+
truncateToWidth(
|
|
2607
|
+
`${statusGlyph(entry.status, theme, Date.now())} ${workflowCompletionSummary(entry)}`,
|
|
2608
|
+
width,
|
|
2609
|
+
"…",
|
|
2610
|
+
),
|
|
2611
|
+
);
|
|
2612
|
+
for (const alert of workflowCompletionAlerts(entry)) {
|
|
2613
|
+
rows.push(
|
|
2614
|
+
truncateToWidth(` ${theme.fg("error", alert)}`, width, "…"),
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
const result = workflowCompletionResultPreview(entry);
|
|
2618
|
+
if (result) {
|
|
2619
|
+
rows.push(
|
|
2620
|
+
truncateToWidth(
|
|
2621
|
+
` ${theme.fg("accent", "Result:")} ${result}`,
|
|
2622
|
+
width,
|
|
2623
|
+
"…",
|
|
2624
|
+
),
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
rows.push(
|
|
2629
|
+
truncateToWidth(
|
|
2630
|
+
theme.fg(
|
|
2631
|
+
"muted",
|
|
2632
|
+
`(${keyHint("app.tools.expand", "to expand")})`,
|
|
2633
|
+
),
|
|
2634
|
+
width,
|
|
2635
|
+
"…",
|
|
2636
|
+
),
|
|
2637
|
+
);
|
|
2638
|
+
return rows;
|
|
2639
|
+
},
|
|
2640
|
+
invalidate() {},
|
|
2641
|
+
};
|
|
2092
2642
|
},
|
|
2093
2643
|
);
|
|
2094
2644
|
}
|