@tt-a1i/openpi 0.3.0 → 0.4.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 +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -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/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- 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,26 +33,41 @@ 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
|
-
|
|
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";
|
|
45
51
|
import { formatActivityStatus } from "../shared/activity-status.ts";
|
|
46
52
|
import { waitBounded } from "../shared/child-session.ts";
|
|
53
|
+
import { contextPercent } from "../shared/context-utilization.ts";
|
|
47
54
|
import {
|
|
48
55
|
registerEditorLayer,
|
|
49
56
|
removeEditorLayer,
|
|
50
57
|
} from "../shared/editor-layers.ts";
|
|
58
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
51
59
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
60
|
+
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
|
|
52
61
|
import {
|
|
53
62
|
OPENPI_TOOL_SURFACE,
|
|
54
63
|
patchOwnedTools,
|
|
55
64
|
} from "../shared/tool-surface.ts";
|
|
65
|
+
import {
|
|
66
|
+
createWorktree,
|
|
67
|
+
reclaimWorktree,
|
|
68
|
+
type Worktree,
|
|
69
|
+
type WorktreeCleanup,
|
|
70
|
+
} from "../shared/worktree.ts";
|
|
56
71
|
import {
|
|
57
72
|
loadAgentTypes,
|
|
58
73
|
resolveAgentModel,
|
|
@@ -60,16 +75,30 @@ import {
|
|
|
60
75
|
selectSubagentModel,
|
|
61
76
|
} from "../subagents/src/agent-types.ts";
|
|
62
77
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
acceptanceInstruction,
|
|
79
|
+
acceptanceSchema,
|
|
80
|
+
applyAcceptance,
|
|
81
|
+
evaluateAcceptance,
|
|
82
|
+
parseAcceptanceContract,
|
|
83
|
+
} from "./acceptance.ts";
|
|
68
84
|
import {
|
|
69
85
|
createWorkflowPersistence,
|
|
70
86
|
loadJournal,
|
|
87
|
+
persistWorkflowAgentResult,
|
|
71
88
|
persistWorkflowJson,
|
|
72
89
|
} from "./artifacts.ts";
|
|
90
|
+
import { RunController } from "./controller.ts";
|
|
91
|
+
import {
|
|
92
|
+
resolveWorkflowLaunchPolicy,
|
|
93
|
+
waitForWorkflowCompletion,
|
|
94
|
+
} from "./coordinator.ts";
|
|
95
|
+
import {
|
|
96
|
+
listPersistedRunIds,
|
|
97
|
+
readPersistedWorkflowDetails,
|
|
98
|
+
recoverStaleWorkflowDetails,
|
|
99
|
+
sessionWorkflowRunIds,
|
|
100
|
+
showWorkflowDashboard,
|
|
101
|
+
} from "./dashboard.ts";
|
|
73
102
|
import { createWorkflowHandoffRegistry } from "./handoff.ts";
|
|
74
103
|
import {
|
|
75
104
|
classifyInterruptedInvocation,
|
|
@@ -77,57 +106,59 @@ import {
|
|
|
77
106
|
requestInvocation,
|
|
78
107
|
transitionInvocation,
|
|
79
108
|
} from "./invocation-ledger.ts";
|
|
80
|
-
import {
|
|
81
|
-
normalizeWorkflowOperatorKey,
|
|
82
|
-
WorkflowOperatorRegistry,
|
|
83
|
-
} from "./operator.ts";
|
|
84
109
|
import {
|
|
85
110
|
agentCallKey,
|
|
86
111
|
createReplayCache,
|
|
87
112
|
type JournalEntry,
|
|
88
113
|
type ReplayCache,
|
|
89
114
|
} from "./journal.ts";
|
|
90
|
-
import { RunController } from "./controller.ts";
|
|
91
|
-
import {
|
|
92
|
-
normalizePersistedWorkflowDetails,
|
|
93
|
-
recoverStaleWorkflowDetails,
|
|
94
|
-
sessionWorkflowRunIds,
|
|
95
|
-
showWorkflowDashboard,
|
|
96
|
-
} from "./dashboard.ts";
|
|
97
115
|
import {
|
|
98
116
|
extractMeta,
|
|
99
117
|
prepareWorkflowScript,
|
|
100
118
|
type WorkflowMeta,
|
|
101
119
|
} from "./meta.ts";
|
|
102
120
|
import {
|
|
121
|
+
type AgentRecord,
|
|
103
122
|
agentContext,
|
|
104
123
|
aggregateUsage,
|
|
105
124
|
appendLog,
|
|
106
125
|
countStates,
|
|
126
|
+
createUsageReader,
|
|
107
127
|
emptyUsage,
|
|
108
128
|
formatElapsed,
|
|
109
129
|
formatUsage,
|
|
110
130
|
isWorkflowRunId,
|
|
111
131
|
phaseGroups,
|
|
132
|
+
refreshWorkflowGraph,
|
|
112
133
|
resolveWorkflowRunTarget,
|
|
113
134
|
resultJson,
|
|
114
135
|
sanitizeLine,
|
|
115
136
|
sanitizeWorkflowDisplayLine,
|
|
116
137
|
sanitizeWorkflowDisplayText,
|
|
117
|
-
|
|
138
|
+
stateGlyph,
|
|
118
139
|
statusColor,
|
|
140
|
+
statusGlyph,
|
|
119
141
|
statusWord,
|
|
120
|
-
createUsageReader,
|
|
121
|
-
refreshWorkflowGraph,
|
|
122
|
-
SQUARE,
|
|
123
|
-
type AgentRecord,
|
|
124
142
|
type WorkflowDetails,
|
|
125
143
|
} from "./model.ts";
|
|
144
|
+
import {
|
|
145
|
+
WorkflowNavigationEditor,
|
|
146
|
+
type WorkflowStripEntry,
|
|
147
|
+
WorkflowStripState,
|
|
148
|
+
WorkflowStripWidget,
|
|
149
|
+
} from "./navigation.ts";
|
|
150
|
+
import {
|
|
151
|
+
normalizeWorkflowOperatorKey,
|
|
152
|
+
WorkflowOperatorRegistry,
|
|
153
|
+
} from "./operator.ts";
|
|
126
154
|
import {
|
|
127
155
|
buildBackgroundWorkflowFollowUp,
|
|
128
156
|
buildBackgroundWorkflowLaunchResult,
|
|
157
|
+
buildProjectedWorkflowCompletionBatch,
|
|
158
|
+
buildProjectedWorkflowResultMessage,
|
|
129
159
|
buildWorkflowAgentPrompt,
|
|
130
160
|
buildWorkflowResultMessage,
|
|
161
|
+
buildWorkflowStatusSummary,
|
|
131
162
|
WORKFLOW_LIFECYCLE_PROMPT_SNIPPET,
|
|
132
163
|
WORKFLOW_PARAMETER_DESCRIPTIONS,
|
|
133
164
|
WORKFLOW_PROMPT_GUIDELINES,
|
|
@@ -139,39 +170,331 @@ import {
|
|
|
139
170
|
WORKFLOW_TOOL_DESCRIPTION,
|
|
140
171
|
} from "./prompt.ts";
|
|
141
172
|
import {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
createWorkflowResultDelivery,
|
|
174
|
+
type WorkflowCompletionEnvelope,
|
|
175
|
+
} from "./result-delivery.ts";
|
|
176
|
+
import {
|
|
177
|
+
beginProcessReplayWorkspaceLease,
|
|
178
|
+
createReplayIdentity,
|
|
179
|
+
isReplaySafeAgentCall,
|
|
180
|
+
} from "./replay-safety.ts";
|
|
147
181
|
import {
|
|
148
182
|
createWorkflowResources,
|
|
149
183
|
runAgent,
|
|
150
184
|
type ThinkingLevel,
|
|
185
|
+
type WorkflowAgentSessionFactory,
|
|
151
186
|
type WorkflowModel,
|
|
152
187
|
} from "./runner.ts";
|
|
153
|
-
import {
|
|
154
|
-
beginProcessReplayWorkspaceLease,
|
|
155
|
-
createReplayIdentity,
|
|
156
|
-
isReplaySafeAgentCall,
|
|
157
|
-
} from "./replay-safety.ts";
|
|
158
188
|
import { runWorkflowSandbox } from "./sandbox.ts";
|
|
159
|
-
import {
|
|
160
|
-
acceptanceInstruction,
|
|
161
|
-
acceptanceSchema,
|
|
162
|
-
applyAcceptance,
|
|
163
|
-
evaluateAcceptance,
|
|
164
|
-
parseAcceptanceContract,
|
|
165
|
-
} from "./acceptance.ts";
|
|
189
|
+
import { safeStringify, writeFileAtomic } from "./serialization.ts";
|
|
166
190
|
import {
|
|
167
191
|
finalizeWorktreeHandoff,
|
|
168
192
|
prepareWorktreeHandoff,
|
|
169
193
|
} from "./worktree-handoff.ts";
|
|
170
|
-
import { safeStringify, writeFileAtomic } from "./serialization.ts";
|
|
171
194
|
|
|
172
195
|
const PREVIEW_LENGTH = 200;
|
|
173
196
|
const EMIT_INTERVAL_MS = 120;
|
|
174
197
|
|
|
198
|
+
/** Header of a workflow card: identity and phase on the left, metrics right. */
|
|
199
|
+
function runHeader(
|
|
200
|
+
details: WorkflowDetails,
|
|
201
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
202
|
+
now: number,
|
|
203
|
+
) {
|
|
204
|
+
const { done, failed, uncertain } = countStates(details);
|
|
205
|
+
const settled = done + failed;
|
|
206
|
+
const elapsed = formatElapsed(details.startedAt, details.finishedAt);
|
|
207
|
+
// A just-launched run has no agents and a 0s clock; the metrics join in
|
|
208
|
+
// once there is something real to report.
|
|
209
|
+
const counts =
|
|
210
|
+
details.agents.length > 0
|
|
211
|
+
? `${settled}/${details.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""}`
|
|
212
|
+
: undefined;
|
|
213
|
+
const metrics = [counts, counts || elapsed !== "0s" ? elapsed : undefined]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join(" · ");
|
|
216
|
+
let left =
|
|
217
|
+
`${statusGlyph(details.status, theme, now)} ${theme.fg("toolTitle", theme.bold("workflow "))}` +
|
|
218
|
+
theme.fg(
|
|
219
|
+
"accent",
|
|
220
|
+
sanitizeWorkflowDisplayLine(details.name ?? details.runId),
|
|
221
|
+
);
|
|
222
|
+
if (details.status === "running" && details.currentPhase) {
|
|
223
|
+
left += theme.fg(
|
|
224
|
+
"muted",
|
|
225
|
+
` · ${sanitizeWorkflowDisplayLine(details.currentPhase)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
// The glyph already carries the run state, so the status word only stays
|
|
229
|
+
// for terminal states.
|
|
230
|
+
let right = theme.fg("dim", metrics);
|
|
231
|
+
if (details.status !== "running") {
|
|
232
|
+
right +=
|
|
233
|
+
theme.fg("dim", `${metrics ? " · " : ""}`) +
|
|
234
|
+
theme.fg(statusColor(details.status), statusWord(details.status));
|
|
235
|
+
}
|
|
236
|
+
if (failed) right += theme.fg("error", ` · ${failed} failed`);
|
|
237
|
+
return { left, right };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Collapsed workflow card, rebuilt per repaint. Metrics right-align like the
|
|
242
|
+
* below-editor strip, and each agent row carries one number only: context
|
|
243
|
+
* occupancy says a running agent is alive, elapsed says what a settled one
|
|
244
|
+
* cost. A swarm shows the first few agents and summarizes the rest instead
|
|
245
|
+
* of flooding the chat.
|
|
246
|
+
*/
|
|
247
|
+
function buildCollapsedRows(
|
|
248
|
+
details: WorkflowDetails,
|
|
249
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
250
|
+
width: number,
|
|
251
|
+
now: number,
|
|
252
|
+
totals: string,
|
|
253
|
+
) {
|
|
254
|
+
const header = runHeader(details, theme, now);
|
|
255
|
+
const rows = [fitNavigationSides(header.left, header.right, width)];
|
|
256
|
+
const collapsedAgents = details.agents.slice(0, 8);
|
|
257
|
+
for (const agent of collapsedAgents) {
|
|
258
|
+
const percent = contextPercent({
|
|
259
|
+
tokens: agent.usage.contextTokens,
|
|
260
|
+
contextWindow: agent.contextWindow,
|
|
261
|
+
});
|
|
262
|
+
const stat =
|
|
263
|
+
agent.state === "running"
|
|
264
|
+
? percent === undefined
|
|
265
|
+
? undefined
|
|
266
|
+
: `${percent}%`
|
|
267
|
+
: formatElapsed(agent.startedAt, agent.finishedAt);
|
|
268
|
+
const left = ` ${stateGlyph(agent.state, theme, now)} ${theme.fg(
|
|
269
|
+
"accent",
|
|
270
|
+
sanitizeWorkflowDisplayLine(agent.label),
|
|
271
|
+
)}`;
|
|
272
|
+
rows.push(
|
|
273
|
+
stat
|
|
274
|
+
? fitNavigationSides(left, theme.fg("dim", stat), width)
|
|
275
|
+
: truncateToWidth(left, width, "…"),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const hiddenAgents = details.agents.length - collapsedAgents.length;
|
|
279
|
+
if (hiddenAgents > 0) {
|
|
280
|
+
rows.push(` ${theme.fg("dim", `… ${hiddenAgents} more`)}`);
|
|
281
|
+
}
|
|
282
|
+
// Only the tail collapsed: the newest lines are the ones that say where
|
|
283
|
+
// the run is now.
|
|
284
|
+
for (const entry of (details.logs ?? []).slice(-3)) {
|
|
285
|
+
rows.push(
|
|
286
|
+
truncateToWidth(
|
|
287
|
+
` ${theme.fg("muted", "›")} ${theme.fg(
|
|
288
|
+
"dim",
|
|
289
|
+
sanitizeWorkflowDisplayLine(entry.text),
|
|
290
|
+
)}`,
|
|
291
|
+
width,
|
|
292
|
+
"…",
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (totals) rows.push(` ${theme.fg("dim", `Total: ${totals}`)}`);
|
|
297
|
+
if (details.error) {
|
|
298
|
+
rows.push(
|
|
299
|
+
truncateToWidth(
|
|
300
|
+
` ${theme.fg(
|
|
301
|
+
"error",
|
|
302
|
+
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
303
|
+
)}`,
|
|
304
|
+
width,
|
|
305
|
+
"…",
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
// Expanding only earns its hint when there is more to see.
|
|
310
|
+
if (
|
|
311
|
+
details.agents.length > 0 ||
|
|
312
|
+
(details.logs ?? []).length > 0 ||
|
|
313
|
+
details.description ||
|
|
314
|
+
details.result !== undefined ||
|
|
315
|
+
details.error
|
|
316
|
+
) {
|
|
317
|
+
rows.push(
|
|
318
|
+
theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return rows;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildExpandedWorkflow(
|
|
325
|
+
details: WorkflowDetails,
|
|
326
|
+
theme: Parameters<typeof statusGlyph>[1],
|
|
327
|
+
now: number,
|
|
328
|
+
totals: string,
|
|
329
|
+
) {
|
|
330
|
+
const header = runHeader(details, theme, now);
|
|
331
|
+
const container = new Container();
|
|
332
|
+
container.addChild(
|
|
333
|
+
new Text(
|
|
334
|
+
header.right ? `${header.left} ${header.right}` : header.left,
|
|
335
|
+
0,
|
|
336
|
+
0,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
if (details.description) {
|
|
340
|
+
container.addChild(
|
|
341
|
+
new Text(
|
|
342
|
+
theme.fg("dim", sanitizeWorkflowDisplayLine(details.description)),
|
|
343
|
+
0,
|
|
344
|
+
0,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const group of phaseGroups(details)) {
|
|
350
|
+
container.addChild(new Spacer(1));
|
|
351
|
+
container.addChild(
|
|
352
|
+
new Text(
|
|
353
|
+
theme.fg(
|
|
354
|
+
"muted",
|
|
355
|
+
`─── ${sanitizeWorkflowDisplayLine(group.title)} ───`,
|
|
356
|
+
),
|
|
357
|
+
0,
|
|
358
|
+
0,
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
for (const agent of group.agents) {
|
|
362
|
+
const usage = formatUsage(agent.usage, agent.model);
|
|
363
|
+
const context = agentContext(agent);
|
|
364
|
+
let line = `${stateGlyph(agent.state, theme, now)} ${theme.fg(
|
|
365
|
+
"accent",
|
|
366
|
+
sanitizeWorkflowDisplayLine(agent.label),
|
|
367
|
+
)} ${theme.fg(
|
|
368
|
+
"dim",
|
|
369
|
+
[context, formatElapsed(agent.startedAt, agent.finishedAt)]
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join(" · "),
|
|
372
|
+
)}`;
|
|
373
|
+
if (usage)
|
|
374
|
+
line += ` ${theme.fg("dim", sanitizeWorkflowDisplayLine(usage))}`;
|
|
375
|
+
container.addChild(new Text(line, 0, 0));
|
|
376
|
+
if (agent.error) {
|
|
377
|
+
container.addChild(
|
|
378
|
+
new Text(
|
|
379
|
+
` ${theme.fg("error", sanitizeWorkflowDisplayLine(agent.error))}`,
|
|
380
|
+
0,
|
|
381
|
+
0,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
} else if (agent.preview) {
|
|
385
|
+
const preview = sanitizeWorkflowDisplayText(
|
|
386
|
+
agent.preview,
|
|
387
|
+
PREVIEW_LENGTH,
|
|
388
|
+
)
|
|
389
|
+
.split("\n")
|
|
390
|
+
.slice(0, 2)
|
|
391
|
+
.join(" ");
|
|
392
|
+
container.addChild(new Text(` ${theme.fg("dim", preview)}`, 0, 0));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (details.logs && details.logs.length > 0) {
|
|
398
|
+
container.addChild(new Spacer(1));
|
|
399
|
+
container.addChild(new Text(theme.fg("muted", "─── log ───"), 0, 0));
|
|
400
|
+
if (details.logsDropped) {
|
|
401
|
+
container.addChild(
|
|
402
|
+
new Text(
|
|
403
|
+
theme.fg("dim", `(${details.logsDropped} earlier line(s) dropped)`),
|
|
404
|
+
0,
|
|
405
|
+
0,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
for (const entry of details.logs) {
|
|
410
|
+
container.addChild(
|
|
411
|
+
new Text(
|
|
412
|
+
`${theme.fg("muted", "›")} ${theme.fg(
|
|
413
|
+
"dim",
|
|
414
|
+
sanitizeWorkflowDisplayLine(entry.text),
|
|
415
|
+
)}`,
|
|
416
|
+
0,
|
|
417
|
+
0,
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (details.error) {
|
|
424
|
+
container.addChild(new Spacer(1));
|
|
425
|
+
container.addChild(
|
|
426
|
+
new Text(
|
|
427
|
+
theme.fg(
|
|
428
|
+
"error",
|
|
429
|
+
`Error: ${sanitizeWorkflowDisplayLine(details.error)}`,
|
|
430
|
+
),
|
|
431
|
+
0,
|
|
432
|
+
0,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (details.result !== undefined) {
|
|
438
|
+
container.addChild(new Spacer(1));
|
|
439
|
+
container.addChild(new Text(theme.fg("muted", "─── result ───"), 0, 0));
|
|
440
|
+
container.addChild(
|
|
441
|
+
new Markdown(
|
|
442
|
+
`\`\`\`json\n${resultJson(details.result)}\n\`\`\``,
|
|
443
|
+
0,
|
|
444
|
+
0,
|
|
445
|
+
getMarkdownTheme(),
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (totals) {
|
|
451
|
+
container.addChild(new Spacer(1));
|
|
452
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${totals}`), 0, 0));
|
|
453
|
+
}
|
|
454
|
+
return container;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
interface WorkflowRenderState {
|
|
458
|
+
spinnerTimer?: ReturnType<typeof setInterval>;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function syncWorkflowSpinner(
|
|
462
|
+
state: WorkflowRenderState,
|
|
463
|
+
isRunning: () => boolean,
|
|
464
|
+
invalidate: () => void,
|
|
465
|
+
) {
|
|
466
|
+
if (!isRunning()) {
|
|
467
|
+
if (state.spinnerTimer) clearInterval(state.spinnerTimer);
|
|
468
|
+
state.spinnerTimer = undefined;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (state.spinnerTimer) return;
|
|
472
|
+
const spinnerTimer = setInterval(() => {
|
|
473
|
+
if (state.spinnerTimer !== spinnerTimer) return;
|
|
474
|
+
if (!isRunning()) {
|
|
475
|
+
clearInterval(spinnerTimer);
|
|
476
|
+
state.spinnerTimer = undefined;
|
|
477
|
+
}
|
|
478
|
+
invalidate();
|
|
479
|
+
}, SPINNER_INTERVAL_MS);
|
|
480
|
+
state.spinnerTimer = spinnerTimer;
|
|
481
|
+
spinnerTimer.unref?.();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Test-only injection seam for execute-level tests: production never sets it.
|
|
486
|
+
* The underscore-prefixed setter name makes any accidental production use
|
|
487
|
+
* self-evidently wrong.
|
|
488
|
+
*/
|
|
489
|
+
let testAgentSessionFactory: WorkflowAgentSessionFactory | undefined;
|
|
490
|
+
|
|
491
|
+
/** Test-only: override how workflow children create their agent sessions. */
|
|
492
|
+
export function __setWorkflowTestAgentSessionFactory(
|
|
493
|
+
factory: WorkflowAgentSessionFactory | undefined,
|
|
494
|
+
) {
|
|
495
|
+
testAgentSessionFactory = factory;
|
|
496
|
+
}
|
|
497
|
+
|
|
175
498
|
const THINKING_LEVELS = [
|
|
176
499
|
"off",
|
|
177
500
|
"minimal",
|
|
@@ -223,6 +546,11 @@ const WorkflowParams = Type.Object({
|
|
|
223
546
|
description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
|
|
224
547
|
}),
|
|
225
548
|
),
|
|
549
|
+
wait: Type.Optional(
|
|
550
|
+
Type.Boolean({
|
|
551
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
|
|
552
|
+
}),
|
|
553
|
+
),
|
|
226
554
|
resume_from_run_id: Type.Optional(
|
|
227
555
|
Type.String({
|
|
228
556
|
description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
|
|
@@ -267,12 +595,12 @@ function errorText(error: unknown): string {
|
|
|
267
595
|
}
|
|
268
596
|
|
|
269
597
|
function summaryLine(details: WorkflowDetails): string {
|
|
270
|
-
const { done, failed } = countStates(details);
|
|
598
|
+
const { done, failed, uncertain } = countStates(details);
|
|
271
599
|
const settled = done + failed;
|
|
272
600
|
// The newest narrator line beats the phase title when there is one: the
|
|
273
601
|
// script wrote it precisely because it says more than the phase does.
|
|
274
602
|
const latest = details.logs?.[details.logs.length - 1]?.text;
|
|
275
|
-
return `workflow ${details.name ?? details.runId}: ${settled}/${details.agents.length} agents${
|
|
603
|
+
return `workflow ${details.name ?? details.runId}: ${settled}/${details.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""}${
|
|
276
604
|
latest
|
|
277
605
|
? ` · ${latest}`
|
|
278
606
|
: details.currentPhase
|
|
@@ -348,15 +676,8 @@ function listRuns(
|
|
|
348
676
|
referencedRunIds: ReadonlySet<string>,
|
|
349
677
|
startedSince = 0,
|
|
350
678
|
): 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
679
|
const summaries: RunSummary[] = [];
|
|
359
|
-
for (const runId of
|
|
680
|
+
for (const runId of listPersistedRunIds()) {
|
|
360
681
|
const live = activeRuns.get(runId);
|
|
361
682
|
if (live) {
|
|
362
683
|
const { done, failed } = countStates(live);
|
|
@@ -371,34 +692,30 @@ function listRuns(
|
|
|
371
692
|
});
|
|
372
693
|
continue;
|
|
373
694
|
}
|
|
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.
|
|
695
|
+
// Same reader as the dashboard and workflow_status: old-format runs are
|
|
696
|
+
// normalized here too, so every surface reports one status per run.
|
|
697
|
+
const details = readPersistedWorkflowDetails(runId, {
|
|
698
|
+
hydrateArtifacts: false,
|
|
699
|
+
});
|
|
700
|
+
if (!details) continue;
|
|
701
|
+
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
702
|
+
if (
|
|
703
|
+
touchedAt < startedSince ||
|
|
704
|
+
(details.sessionId !== sessionId && !referencedRunIds.has(runId))
|
|
705
|
+
) {
|
|
706
|
+
continue;
|
|
401
707
|
}
|
|
708
|
+
recoverStaleWorkflowDetails(details);
|
|
709
|
+
const { done, failed } = countStates(details);
|
|
710
|
+
summaries.push({
|
|
711
|
+
runId,
|
|
712
|
+
name: details.name,
|
|
713
|
+
status: details.status,
|
|
714
|
+
done: done + failed,
|
|
715
|
+
total: details.agents.length,
|
|
716
|
+
startedAt: details.startedAt,
|
|
717
|
+
active: false,
|
|
718
|
+
});
|
|
402
719
|
}
|
|
403
720
|
return summaries.sort((a, b) => b.startedAt - a.startedAt);
|
|
404
721
|
}
|
|
@@ -410,14 +727,15 @@ function runDetailText(
|
|
|
410
727
|
const runDir = path.join(getAgentDir(), "workflows", run.runId);
|
|
411
728
|
const live = activeRuns.get(run.runId);
|
|
412
729
|
if (live) return buildWorkflowResultMessage(live, runDir);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return buildWorkflowResultMessage(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
730
|
+
const details = readPersistedWorkflowDetails(run.runId, {
|
|
731
|
+
hydrateArtifacts: true,
|
|
732
|
+
});
|
|
733
|
+
if (details)
|
|
734
|
+
return buildWorkflowResultMessage(
|
|
735
|
+
recoverStaleWorkflowDetails(details),
|
|
736
|
+
runDir,
|
|
737
|
+
);
|
|
738
|
+
return `Run ${run.runId} — ${run.status}`;
|
|
421
739
|
}
|
|
422
740
|
|
|
423
741
|
export default function workflows(pi: ExtensionAPI) {
|
|
@@ -428,13 +746,9 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
428
746
|
[...activeRuns].map(([runId, run]) => [runId, run.details] as const),
|
|
429
747
|
);
|
|
430
748
|
const settledRuns = new Map<string, WorkflowDetails>();
|
|
431
|
-
const
|
|
432
|
-
patchOwnedTools(pi, "workflows", {
|
|
433
|
-
disable: OPENPI_TOOL_SURFACE.workflows.deferred,
|
|
434
|
-
});
|
|
435
|
-
const showLifecycleTools = () =>
|
|
749
|
+
const registerStableToolFamily = () =>
|
|
436
750
|
patchOwnedTools(pi, "workflows", {
|
|
437
|
-
enable: OPENPI_TOOL_SURFACE.workflows.
|
|
751
|
+
enable: OPENPI_TOOL_SURFACE.workflows.entry,
|
|
438
752
|
});
|
|
439
753
|
const stripState = new WorkflowStripState();
|
|
440
754
|
const widgetKey = "workflow-navigation";
|
|
@@ -444,6 +758,52 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
444
758
|
* next explicit request acknowledges them.
|
|
445
759
|
*/
|
|
446
760
|
let lastContext: ExtensionContext | undefined;
|
|
761
|
+
const completionEnvelope = (
|
|
762
|
+
details: WorkflowDetails,
|
|
763
|
+
): WorkflowCompletionEnvelope => {
|
|
764
|
+
const deliveryId = details.delivery?.id;
|
|
765
|
+
if (!deliveryId) throw new Error("Workflow delivery identity is missing");
|
|
766
|
+
return {
|
|
767
|
+
deliveryId,
|
|
768
|
+
runId: details.runId,
|
|
769
|
+
details,
|
|
770
|
+
};
|
|
771
|
+
};
|
|
772
|
+
const resultDelivery = createWorkflowResultDelivery({
|
|
773
|
+
isIdle: () => lastContext?.isIdle() ?? false,
|
|
774
|
+
persist: (details) =>
|
|
775
|
+
persistWorkflowJson(
|
|
776
|
+
path.join(getAgentDir(), "workflows", details.runId),
|
|
777
|
+
details,
|
|
778
|
+
),
|
|
779
|
+
deliver: async (envelopes, wake) => {
|
|
780
|
+
const content = buildProjectedWorkflowCompletionBatch(
|
|
781
|
+
envelopes.map((envelope) => ({
|
|
782
|
+
deliveryId: envelope.deliveryId,
|
|
783
|
+
details: envelope.details,
|
|
784
|
+
runDir: path.join(getAgentDir(), "workflows", envelope.runId),
|
|
785
|
+
})),
|
|
786
|
+
lastContext?.getContextUsage?.(),
|
|
787
|
+
);
|
|
788
|
+
pi.sendMessage(
|
|
789
|
+
{
|
|
790
|
+
customType: "workflow-result",
|
|
791
|
+
content,
|
|
792
|
+
display: true,
|
|
793
|
+
...(envelopes.length === 1
|
|
794
|
+
? { details: compactToolDetails(envelopes[0]!.details) }
|
|
795
|
+
: {}),
|
|
796
|
+
},
|
|
797
|
+
wake
|
|
798
|
+
? { deliverAs: "followUp", triggerTurn: true }
|
|
799
|
+
: { deliverAs: "nextTurn" },
|
|
800
|
+
);
|
|
801
|
+
return envelopes.map((envelope) => ({
|
|
802
|
+
deliveryId: envelope.deliveryId,
|
|
803
|
+
delivered: true,
|
|
804
|
+
}));
|
|
805
|
+
},
|
|
806
|
+
});
|
|
447
807
|
let completedRuns = 0;
|
|
448
808
|
let failedRuns = 0;
|
|
449
809
|
let widgetVisible = false;
|
|
@@ -593,7 +953,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
593
953
|
};
|
|
594
954
|
|
|
595
955
|
pi.on("session_start", (_event, ctx) => {
|
|
596
|
-
|
|
956
|
+
registerStableToolFamily();
|
|
597
957
|
if (ctx.hasUI) lastContext = ctx;
|
|
598
958
|
agentTypes = loadAgentTypes({
|
|
599
959
|
agentDir: getAgentDir(),
|
|
@@ -606,6 +966,35 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
606
966
|
settledRuns.clear();
|
|
607
967
|
installWorkflowNavigation(ctx);
|
|
608
968
|
updateIndicator();
|
|
969
|
+
|
|
970
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
971
|
+
for (const runId of listPersistedRunIds()) {
|
|
972
|
+
const details = readPersistedWorkflowDetails(runId, {
|
|
973
|
+
hydrateArtifacts: true,
|
|
974
|
+
});
|
|
975
|
+
if (!details || details.sessionId !== sessionId) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const wasRunning = details.status === "running";
|
|
979
|
+
if (wasRunning) {
|
|
980
|
+
recoverStaleWorkflowDetails(details);
|
|
981
|
+
persistWorkflowJson(
|
|
982
|
+
path.join(getAgentDir(), "workflows", details.runId),
|
|
983
|
+
details,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
// Pre-V2 terminal runs have no delivery receipt. They may already have
|
|
987
|
+
// been shown, so replaying them on upgrade would be a surprising
|
|
988
|
+
// duplicate. Only migrate owner-lost runs, whose uncertainty matters.
|
|
989
|
+
if (details.delivery) {
|
|
990
|
+
resultDelivery.restore(completionEnvelope(details));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
void resultDelivery.flushIfIdle();
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
pi.on("agent_settled", () => {
|
|
997
|
+
void resultDelivery.parentSettled();
|
|
609
998
|
});
|
|
610
999
|
|
|
611
1000
|
pi.on("input", (event) => {
|
|
@@ -631,6 +1020,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
631
1020
|
widgetVisible = false;
|
|
632
1021
|
requestWidgetRender = undefined;
|
|
633
1022
|
stripState.focused = false;
|
|
1023
|
+
resultDelivery.clear();
|
|
634
1024
|
});
|
|
635
1025
|
|
|
636
1026
|
pi.registerCommand("workflows", {
|
|
@@ -736,7 +1126,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
736
1126
|
const meta = prepared.meta;
|
|
737
1127
|
const runId = `wf_${randomBytes(6).toString("hex")}`;
|
|
738
1128
|
const runDir = path.join(getAgentDir(), "workflows", runId);
|
|
739
|
-
const
|
|
1129
|
+
const canDeliverLater = ctx.hasUI && ctx.mode === "tui";
|
|
1130
|
+
const launchPolicy = resolveWorkflowLaunchPolicy(
|
|
1131
|
+
{ wait: params.wait, background: params.background },
|
|
1132
|
+
canDeliverLater,
|
|
1133
|
+
);
|
|
1134
|
+
const background = launchPolicy.detached;
|
|
1135
|
+
const now = Date.now();
|
|
740
1136
|
|
|
741
1137
|
const details: WorkflowDetails = {
|
|
742
1138
|
runId,
|
|
@@ -745,9 +1141,15 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
745
1141
|
description: meta.description,
|
|
746
1142
|
background,
|
|
747
1143
|
status: "running",
|
|
748
|
-
startedAt:
|
|
1144
|
+
startedAt: now,
|
|
749
1145
|
phases: [...meta.phases],
|
|
750
1146
|
agents: [],
|
|
1147
|
+
delivery: {
|
|
1148
|
+
id: `workflow:${runId}:terminal`,
|
|
1149
|
+
state: launchPolicy.wait ? "held-for-inline" : "none",
|
|
1150
|
+
attempts: 0,
|
|
1151
|
+
updatedAt: now,
|
|
1152
|
+
},
|
|
751
1153
|
};
|
|
752
1154
|
|
|
753
1155
|
// Resume: replay cached results for calls whose content is unchanged.
|
|
@@ -776,13 +1178,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
776
1178
|
journal: () => journalEntries,
|
|
777
1179
|
});
|
|
778
1180
|
|
|
779
|
-
//
|
|
780
|
-
//
|
|
1181
|
+
// A caller wait never owns the run. All runs survive an interrupted
|
|
1182
|
+
// launch turn and are aborted only by workflow_stop/session shutdown.
|
|
781
1183
|
const workflowConfig = loadSetupConfig().workflows;
|
|
782
1184
|
const projectTrusted = ctx.isProjectTrusted();
|
|
783
1185
|
const runAgentTypes = agentTypes;
|
|
784
1186
|
const controller = new RunController(
|
|
785
|
-
|
|
1187
|
+
undefined,
|
|
786
1188
|
workflowConfig.concurrency,
|
|
787
1189
|
workflowConfig.maxAgentCalls,
|
|
788
1190
|
);
|
|
@@ -870,6 +1272,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
870
1272
|
}
|
|
871
1273
|
details.status = status;
|
|
872
1274
|
details.finishedAt = Date.now();
|
|
1275
|
+
if (details.delivery && background) {
|
|
1276
|
+
details.delivery = {
|
|
1277
|
+
...details.delivery,
|
|
1278
|
+
state: "pending",
|
|
1279
|
+
updatedAt: details.finishedAt,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
873
1282
|
refreshWorkflowGraph(details);
|
|
874
1283
|
if (error) details.error = sanitizeWorkflowDisplayLine(error);
|
|
875
1284
|
return true;
|
|
@@ -905,7 +1314,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
905
1314
|
|
|
906
1315
|
// One reader per run: it carries a high-water mark, because per-agent
|
|
907
1316
|
// usage is recomputed from a message list that compaction shrinks.
|
|
908
|
-
const
|
|
1317
|
+
const readBaseUsage = createUsageReader(details.agents);
|
|
1318
|
+
const readUsage = () => ({
|
|
1319
|
+
...readBaseUsage(),
|
|
1320
|
+
limits: controller.capacity(),
|
|
1321
|
+
});
|
|
909
1322
|
|
|
910
1323
|
let agentCounter = 0;
|
|
911
1324
|
const agentFn = async (
|
|
@@ -1239,11 +1652,18 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1239
1652
|
cached.structured,
|
|
1240
1653
|
);
|
|
1241
1654
|
}
|
|
1655
|
+
record.resultArtifact = persistWorkflowAgentResult(runDir, index, {
|
|
1656
|
+
output: cached.output,
|
|
1657
|
+
...(cached.structured !== undefined
|
|
1658
|
+
? { structured: cached.structured }
|
|
1659
|
+
: {}),
|
|
1660
|
+
});
|
|
1242
1661
|
const ref = handoffs.register({
|
|
1243
1662
|
callId,
|
|
1244
1663
|
settled: true,
|
|
1245
1664
|
ok: true,
|
|
1246
1665
|
output: cached.output,
|
|
1666
|
+
resultArtifact: record.resultArtifact,
|
|
1247
1667
|
...(cached.structured !== undefined
|
|
1248
1668
|
? { structured: cached.structured }
|
|
1249
1669
|
: {}),
|
|
@@ -1365,6 +1785,9 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1365
1785
|
...(sessionManager ? { sessionManager } : {}),
|
|
1366
1786
|
modelRegistry: ctx.modelRegistry,
|
|
1367
1787
|
...(agentType?.tools ? { tools: agentType.tools } : {}),
|
|
1788
|
+
...(testAgentSessionFactory
|
|
1789
|
+
? { sessionFactory: testAgentSessionFactory }
|
|
1790
|
+
: {}),
|
|
1368
1791
|
...(replayIdentity
|
|
1369
1792
|
? {
|
|
1370
1793
|
replayFilesystemBoundary: {
|
|
@@ -1441,11 +1864,26 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1441
1864
|
record.error = judged.error
|
|
1442
1865
|
? sanitizeWorkflowDisplayLine(judged.error)
|
|
1443
1866
|
: undefined;
|
|
1867
|
+
if (outcomeOk) {
|
|
1868
|
+
record.resultArtifact = persistWorkflowAgentResult(
|
|
1869
|
+
runDir,
|
|
1870
|
+
index,
|
|
1871
|
+
{
|
|
1872
|
+
output: outcome.output,
|
|
1873
|
+
...(outcome.structured !== undefined
|
|
1874
|
+
? { structured: outcome.structured }
|
|
1875
|
+
: {}),
|
|
1876
|
+
},
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1444
1879
|
const ref = handoffs.register({
|
|
1445
1880
|
callId,
|
|
1446
1881
|
settled: true,
|
|
1447
1882
|
ok: outcomeOk,
|
|
1448
1883
|
output: outcome.output,
|
|
1884
|
+
...(record.resultArtifact
|
|
1885
|
+
? { resultArtifact: record.resultArtifact }
|
|
1886
|
+
: {}),
|
|
1449
1887
|
...(outcome.structured !== undefined
|
|
1450
1888
|
? { structured: outcome.structured }
|
|
1451
1889
|
: {}),
|
|
@@ -1624,48 +2062,29 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1624
2062
|
if (ctx.hasUI) lastContext = ctx;
|
|
1625
2063
|
updateIndicator();
|
|
1626
2064
|
|
|
2065
|
+
const recordTerminalRun = () => {
|
|
2066
|
+
activeRuns.delete(runId);
|
|
2067
|
+
recordSettledRun(details);
|
|
2068
|
+
updateIndicator();
|
|
2069
|
+
};
|
|
2070
|
+
|
|
2071
|
+
const settleForLaterDelivery = async (inlineReleased: boolean) => {
|
|
2072
|
+
try {
|
|
2073
|
+
await completion;
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
details.status = "failed";
|
|
2076
|
+
details.finishedAt = Date.now();
|
|
2077
|
+
details.error = details.error ?? errorText(error);
|
|
2078
|
+
} finally {
|
|
2079
|
+
recordTerminalRun();
|
|
2080
|
+
const envelope = completionEnvelope(details);
|
|
2081
|
+
if (inlineReleased) resultDelivery.releaseInline(envelope);
|
|
2082
|
+
else resultDelivery.defer(envelope);
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
|
|
1627
2086
|
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();
|
|
2087
|
+
void settleForLaterDelivery(false);
|
|
1669
2088
|
return {
|
|
1670
2089
|
content: [
|
|
1671
2090
|
{
|
|
@@ -1681,13 +2100,15 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1681
2100
|
};
|
|
1682
2101
|
}
|
|
1683
2102
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2103
|
+
const waitOutcome = await waitForWorkflowCompletion(completion, signal);
|
|
2104
|
+
if (waitOutcome === "aborted") {
|
|
2105
|
+
void settleForLaterDelivery(true);
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
`Workflow wait interrupted; run ${runId} continues in the background.`,
|
|
2108
|
+
);
|
|
1690
2109
|
}
|
|
2110
|
+
recordTerminalRun();
|
|
2111
|
+
resultDelivery.consumeInline(details);
|
|
1691
2112
|
if (details.status !== "completed") {
|
|
1692
2113
|
// Pi marks tool failures only when execute throws; returning isError is
|
|
1693
2114
|
// ignored by the extension API.
|
|
@@ -1697,7 +2118,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1697
2118
|
content: [
|
|
1698
2119
|
{
|
|
1699
2120
|
type: "text",
|
|
1700
|
-
text:
|
|
2121
|
+
text: buildProjectedWorkflowResultMessage(
|
|
2122
|
+
details,
|
|
2123
|
+
runDir,
|
|
2124
|
+
ctx.getContextUsage?.(),
|
|
2125
|
+
),
|
|
1701
2126
|
},
|
|
1702
2127
|
],
|
|
1703
2128
|
details: compactToolDetails(details),
|
|
@@ -1716,14 +2141,14 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1716
2141
|
const description = (meta as WorkflowMeta).description;
|
|
1717
2142
|
if (description) text += `\n ${theme.fg("dim", description)}`;
|
|
1718
2143
|
for (const phase of meta.phases.slice(0, 8)) {
|
|
1719
|
-
text += `\n ${theme.fg("dim",
|
|
2144
|
+
text += `\n ${theme.fg("dim", "○")} ${theme.fg("accent", phase.title)}${
|
|
1720
2145
|
phase.detail ? theme.fg("dim", ` — ${phase.detail}`) : ""
|
|
1721
2146
|
}`;
|
|
1722
2147
|
}
|
|
1723
2148
|
return new Text(text, 0, 0);
|
|
1724
2149
|
},
|
|
1725
2150
|
|
|
1726
|
-
renderResult(result, { expanded }, theme) {
|
|
2151
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1727
2152
|
const details = result.details as WorkflowDetails | undefined;
|
|
1728
2153
|
if (!details) {
|
|
1729
2154
|
const first = result.content[0];
|
|
@@ -1733,205 +2158,49 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1733
2158
|
0,
|
|
1734
2159
|
);
|
|
1735
2160
|
}
|
|
2161
|
+
const currentDetails = () =>
|
|
2162
|
+
activeRuns.get(details.runId)?.details ??
|
|
2163
|
+
settledRuns.get(details.runId) ??
|
|
2164
|
+
details;
|
|
2165
|
+
syncWorkflowSpinner(
|
|
2166
|
+
context.state as WorkflowRenderState,
|
|
2167
|
+
() =>
|
|
2168
|
+
currentDetails().status === "running" &&
|
|
2169
|
+
(isPartial || activeRuns.has(details.runId)),
|
|
2170
|
+
context.invalidate,
|
|
2171
|
+
);
|
|
1736
2172
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
-
),
|
|
2173
|
+
return {
|
|
2174
|
+
render(width: number) {
|
|
2175
|
+
const current = currentDetails();
|
|
2176
|
+
const totals = formatUsage(aggregateUsage(current.agents));
|
|
2177
|
+
if (!expanded) {
|
|
2178
|
+
return buildCollapsedRows(
|
|
2179
|
+
current,
|
|
2180
|
+
theme,
|
|
2181
|
+
width,
|
|
2182
|
+
Date.now(),
|
|
2183
|
+
totals,
|
|
1844
2184
|
);
|
|
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));
|
|
1854
2185
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
theme.fg(
|
|
1865
|
-
"dim",
|
|
1866
|
-
`(${details.logsDropped} earlier line(s) dropped)`,
|
|
1867
|
-
),
|
|
1868
|
-
0,
|
|
1869
|
-
0,
|
|
1870
|
-
),
|
|
1871
|
-
);
|
|
1872
|
-
}
|
|
1873
|
-
for (const entry of details.logs) {
|
|
1874
|
-
container.addChild(
|
|
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;
|
|
2186
|
+
return buildExpandedWorkflow(
|
|
2187
|
+
current,
|
|
2188
|
+
theme,
|
|
2189
|
+
Date.now(),
|
|
2190
|
+
totals,
|
|
2191
|
+
).render(width);
|
|
2192
|
+
},
|
|
2193
|
+
invalidate() {},
|
|
2194
|
+
};
|
|
1919
2195
|
},
|
|
1920
2196
|
});
|
|
1921
2197
|
|
|
1922
2198
|
/** Resolve one run from live, settled, or persisted state. */
|
|
1923
2199
|
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
2200
|
const resolution = resolveWorkflowRunTarget(target, [
|
|
1932
2201
|
...activeRuns.keys(),
|
|
1933
2202
|
...settledRuns.keys(),
|
|
1934
|
-
...
|
|
2203
|
+
...listPersistedRunIds(),
|
|
1935
2204
|
]);
|
|
1936
2205
|
if (!resolution.ok) return resolution;
|
|
1937
2206
|
|
|
@@ -1940,31 +2209,22 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1940
2209
|
const settled = settledRuns.get(resolution.runId);
|
|
1941
2210
|
if (settled) return { ok: true, details: settled } as const;
|
|
1942
2211
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
"utf8",
|
|
1948
|
-
),
|
|
1949
|
-
);
|
|
1950
|
-
const details = normalizePersistedWorkflowDetails(
|
|
1951
|
-
resolution.runId,
|
|
1952
|
-
parsed,
|
|
1953
|
-
);
|
|
1954
|
-
if (!details) throw new Error("invalid workflow details");
|
|
1955
|
-
// A run absent from activeRuns cannot still be running this session; a
|
|
1956
|
-
// persisted "running" is a run that was hard-killed or missed the
|
|
1957
|
-
// shutdown settle deadline.
|
|
1958
|
-
return {
|
|
1959
|
-
ok: true,
|
|
1960
|
-
details: recoverStaleWorkflowDetails(details),
|
|
1961
|
-
} as const;
|
|
1962
|
-
} catch {
|
|
2212
|
+
const details = readPersistedWorkflowDetails(resolution.runId, {
|
|
2213
|
+
hydrateArtifacts: true,
|
|
2214
|
+
});
|
|
2215
|
+
if (!details) {
|
|
1963
2216
|
return {
|
|
1964
2217
|
ok: false,
|
|
1965
2218
|
error: `Workflow run ${resolution.runId} could not be read.`,
|
|
1966
2219
|
} as const;
|
|
1967
2220
|
}
|
|
2221
|
+
// A run absent from activeRuns cannot still be running this session; a
|
|
2222
|
+
// persisted "running" is a run that was hard-killed or missed the
|
|
2223
|
+
// shutdown settle deadline.
|
|
2224
|
+
return {
|
|
2225
|
+
ok: true,
|
|
2226
|
+
details: recoverStaleWorkflowDetails(details),
|
|
2227
|
+
} as const;
|
|
1968
2228
|
};
|
|
1969
2229
|
|
|
1970
2230
|
pi.registerTool({
|
|
@@ -1978,23 +2238,29 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1978
2238
|
}),
|
|
1979
2239
|
}),
|
|
1980
2240
|
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
|
-
);
|
|
2241
|
+
const resolution = resolveRunDetails(params.runId);
|
|
1988
2242
|
if (!resolution.ok) throw new Error(resolution.error);
|
|
1989
|
-
|
|
2243
|
+
const details = resolution.details;
|
|
2244
|
+
if (details.status !== "running") {
|
|
2245
|
+
return Promise.resolve({
|
|
2246
|
+
content: [
|
|
2247
|
+
{
|
|
2248
|
+
type: "text",
|
|
2249
|
+
text: `Workflow ${details.runId} is already ${details.status}.`,
|
|
2250
|
+
},
|
|
2251
|
+
],
|
|
2252
|
+
details: { runId: details.runId, status: details.status },
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
stopRun(details.runId);
|
|
1990
2256
|
return Promise.resolve({
|
|
1991
2257
|
content: [
|
|
1992
2258
|
{
|
|
1993
2259
|
type: "text",
|
|
1994
|
-
text: `Stopping workflow ${
|
|
2260
|
+
text: `Stopping workflow ${details.runId}.`,
|
|
1995
2261
|
},
|
|
1996
2262
|
],
|
|
1997
|
-
details: { runId:
|
|
2263
|
+
details: { runId: details.runId, status: "aborting" },
|
|
1998
2264
|
});
|
|
1999
2265
|
},
|
|
2000
2266
|
});
|
|
@@ -2014,13 +2280,14 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2014
2280
|
// Details are a uniform run-summary array (one entry for a single-id peek)
|
|
2015
2281
|
// so the tool has a single result shape; the text carries the detail.
|
|
2016
2282
|
const summarize = (d: WorkflowDetails) => {
|
|
2017
|
-
const { done, failed } = countStates(d);
|
|
2283
|
+
const { done, failed, uncertain } = countStates(d);
|
|
2018
2284
|
return {
|
|
2019
2285
|
runId: d.runId,
|
|
2020
2286
|
name: d.name,
|
|
2021
2287
|
status: d.status,
|
|
2022
2288
|
done,
|
|
2023
2289
|
failed,
|
|
2290
|
+
uncertain,
|
|
2024
2291
|
total: d.agents.length,
|
|
2025
2292
|
};
|
|
2026
2293
|
};
|
|
@@ -2031,7 +2298,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2031
2298
|
const runDir = path.join(getAgentDir(), "workflows", details.runId);
|
|
2032
2299
|
return Promise.resolve({
|
|
2033
2300
|
content: [
|
|
2034
|
-
{ type: "text", text:
|
|
2301
|
+
{ type: "text", text: buildWorkflowStatusSummary(details, runDir) },
|
|
2035
2302
|
],
|
|
2036
2303
|
details: { runs: [summarize(details)] },
|
|
2037
2304
|
});
|
|
@@ -2049,8 +2316,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2049
2316
|
});
|
|
2050
2317
|
}
|
|
2051
2318
|
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` : ""}`;
|
|
2319
|
+
const { done, failed, uncertain } = countStates(d);
|
|
2320
|
+
return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`;
|
|
2054
2321
|
});
|
|
2055
2322
|
return Promise.resolve({
|
|
2056
2323
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -2071,17 +2338,10 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2071
2338
|
.join("") ?? "");
|
|
2072
2339
|
const safeBody = sanitizeWorkflowDisplayText(body);
|
|
2073
2340
|
if (!details) return new Text(safeBody, 0, 0);
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
`${theme.fg(
|
|
2079
|
-
"accent",
|
|
2080
|
-
sanitizeWorkflowDisplayLine(details.name ?? details.runId),
|
|
2081
|
-
)} ` +
|
|
2082
|
-
theme.fg("dim", `${settled}/${details.agents.length} agents · `) +
|
|
2083
|
-
theme.fg(statusColor(details.status), statusWord(details.status));
|
|
2084
|
-
if (failed) header += theme.fg("error", ` · ${failed} failed`);
|
|
2341
|
+
const headerParts = runHeader(details, theme, Date.now());
|
|
2342
|
+
const header = headerParts.right
|
|
2343
|
+
? `${headerParts.left} ${headerParts.right}`
|
|
2344
|
+
: headerParts.left;
|
|
2085
2345
|
if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
|
|
2086
2346
|
const preview = safeBody.split("\n").slice(0, 8).join("\n");
|
|
2087
2347
|
return new Text(
|