pi-extensible-workflows 3.1.0 → 3.3.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 +11 -47
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +51 -8
- package/dist/src/host.js +1181 -487
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +19 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +42 -2
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +21 -28
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +13 -4
- package/src/host.ts +992 -447
- package/src/index.ts +4 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +33 -2
- package/src/workflow-artifacts.ts +34 -0
- package/src/workflow-evals.ts +24 -3
package/dist/src/host.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { Type } from "@earendil-works/pi-ai";
|
|
7
8
|
import { Value } from "typebox/value";
|
|
8
|
-
import { copyToClipboard, getAgentDir,
|
|
9
|
+
import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
10
11
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
11
12
|
import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
@@ -14,17 +15,21 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
|
|
|
14
15
|
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
15
16
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
|
|
16
17
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
17
|
-
import {
|
|
18
|
+
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
|
|
19
|
+
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STALL_THRESHOLD_MS, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
|
|
18
20
|
const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
|
|
21
|
+
const INTERNAL_WORKFLOW_TOOLS = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
|
|
22
|
+
const HARD_TERMINAL_RUN_STATES = new Set(["completed", "failed", "stopped"]);
|
|
23
|
+
const SHUTDOWN_TERMINAL_RUN_STATES = new Set(["completed", "failed", "stopped", "budget_exhausted"]);
|
|
19
24
|
function snapshotResourcePolicy(snapshot, cwd, projectTrusted, globalSettingsPath) {
|
|
20
25
|
const empty = { skills: [], extensions: [] };
|
|
21
|
-
return { globalSettingsPath, projectSettingsPath:
|
|
26
|
+
return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: snapshot.settings.disabledAgentResources ?? empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
22
27
|
}
|
|
23
28
|
const PLAIN_WORKFLOW_PROGRESS_STYLES = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
24
29
|
function workflowLaunchSettings(cwd, projectTrusted, globalSettingsPath, concurrency) {
|
|
25
30
|
const resolution = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
26
31
|
const settings = Object.freeze({ ...resolution.effective, ...(concurrency === undefined ? {} : { concurrency }) });
|
|
27
|
-
return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath)
|
|
32
|
+
return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath) };
|
|
28
33
|
}
|
|
29
34
|
function frozenResourcePolicy(policy) { return () => structuredClone(policy); }
|
|
30
35
|
function resumedSnapshotSettings(snapshot, resolution, modelAliases) {
|
|
@@ -81,9 +86,10 @@ function mainAgentError(error) {
|
|
|
81
86
|
const typed = asWorkflowError(error);
|
|
82
87
|
const presented = new WorkflowError(typed.code, formatWorkflowFailure(typed));
|
|
83
88
|
Object.assign(presented, typed);
|
|
84
|
-
presented.message = formatWorkflowFailure(typed);
|
|
85
89
|
return presented;
|
|
86
90
|
}
|
|
91
|
+
function workflowFailedAt(error) { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
|
|
92
|
+
function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
87
93
|
export class RunLifecycle {
|
|
88
94
|
changed;
|
|
89
95
|
#state;
|
|
@@ -144,7 +150,7 @@ export class RunLifecycle {
|
|
|
144
150
|
await this.enter();
|
|
145
151
|
}
|
|
146
152
|
async terminal(state, reason) {
|
|
147
|
-
if (
|
|
153
|
+
if (HARD_TERMINAL_RUN_STATES.has(this.#state))
|
|
148
154
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `${this.#state} runs are terminal`);
|
|
149
155
|
await this.#set(state, reason ?? state);
|
|
150
156
|
for (const resolve of this.#waiters.splice(0))
|
|
@@ -165,20 +171,42 @@ export function formatWorkflowPreview(args) {
|
|
|
165
171
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
166
172
|
}
|
|
167
173
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
168
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
169
|
-
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that
|
|
174
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
175
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
176
|
+
function workflowRecoveryGuidance(action, state) {
|
|
177
|
+
if (action === "resume") {
|
|
178
|
+
if (state === "failed")
|
|
179
|
+
return "Failed workflow runs must use workflow_retry({ runId })";
|
|
180
|
+
if (state === "completed")
|
|
181
|
+
return "Completed workflow runs have no recovery action";
|
|
182
|
+
if (state === "stopped")
|
|
183
|
+
return "Stopped workflow runs have no recovery action; launch a new workflow";
|
|
184
|
+
if (state === "interrupted")
|
|
185
|
+
return "Interrupted workflow runs use /workflow resume, not workflow_resume";
|
|
186
|
+
return `Only budget-exhausted runs can be resumed with workflow_resume; source is ${state}`;
|
|
187
|
+
}
|
|
188
|
+
if (state === "budget_exhausted")
|
|
189
|
+
return "Budget-exhausted workflow runs must use workflow_resume({ runId, budget? })";
|
|
190
|
+
if (state === "completed")
|
|
191
|
+
return "Completed workflow runs have no recovery action";
|
|
192
|
+
if (state === "stopped")
|
|
193
|
+
return "Stopped workflow runs cannot be retried; launch a new workflow";
|
|
194
|
+
if (state === "interrupted")
|
|
195
|
+
return "Interrupted workflow runs use /workflow resume, not workflow_retry";
|
|
196
|
+
return `Only failed workflow runs can be retried; source is ${state}`;
|
|
197
|
+
}
|
|
170
198
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
171
|
-
name: Type.Optional(Type.String({ description: "Required non-empty name for inline workflow
|
|
199
|
+
name: Type.Optional(Type.String({ description: "Required non-empty name for the default inline workflow path; invalid for registered function launches" })),
|
|
172
200
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
173
|
-
script: Type.Optional(Type.String({ description: "Immutable workflow source
|
|
174
|
-
workflow: Type.Optional(Type.String({ description: "
|
|
201
|
+
script: Type.Optional(Type.String({ description: "Immutable inline workflow source; default to a named script that fans out with parallel(...) and awaits results before passing them to a summarizing agent(...)" })),
|
|
202
|
+
workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name" })),
|
|
175
203
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
176
|
-
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of
|
|
177
|
-
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
178
|
-
budget: Type.Optional(Type.Unknown({ description: "
|
|
179
|
-
parentRunId: Type.Optional(Type.String({ description: "
|
|
204
|
+
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
|
|
205
|
+
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
|
|
206
|
+
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
207
|
+
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
180
208
|
});
|
|
181
|
-
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
209
|
+
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
182
210
|
function phaseNames(source) {
|
|
183
211
|
const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
|
|
184
212
|
return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
@@ -258,7 +286,7 @@ export function buildWorkflowPhaseModel(run, source) {
|
|
|
258
286
|
const agents = observation?.agents ?? [];
|
|
259
287
|
const counts = phaseAgentCounts(agents);
|
|
260
288
|
const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1) : "not started";
|
|
261
|
-
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state,
|
|
289
|
+
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
|
|
262
290
|
});
|
|
263
291
|
let currentPhaseIndex;
|
|
264
292
|
for (let index = phases.length - 1; index >= 0; index -= 1) {
|
|
@@ -282,12 +310,197 @@ export function buildWorkflowPhaseModel(run, source) {
|
|
|
282
310
|
result.unassignedAgents = unassignedAgents;
|
|
283
311
|
return result;
|
|
284
312
|
}
|
|
313
|
+
function workflowPhaseTreePath(kind, phaseId, operationPath, agentId) {
|
|
314
|
+
const root = `phase/${encodeURIComponent(phaseId)}`;
|
|
315
|
+
if (kind === "phase")
|
|
316
|
+
return root;
|
|
317
|
+
const operation = operationPath.map((part) => encodeURIComponent(part)).join("/");
|
|
318
|
+
if (kind === "operation")
|
|
319
|
+
return `${root}/operation/${operation}`;
|
|
320
|
+
return operation ? `${root}/operation/${operation}/agent/${encodeURIComponent(agentId ?? "")}` : `${root}/agent/${encodeURIComponent(agentId ?? "")}`;
|
|
321
|
+
}
|
|
322
|
+
function workflowPhaseTreeAggregateState(states) {
|
|
323
|
+
if (!states.length || states.every((state) => state === "completed"))
|
|
324
|
+
return "completed";
|
|
325
|
+
if (states.some((state) => state === "failed"))
|
|
326
|
+
return "failed";
|
|
327
|
+
if (states.some((state) => state === "cancelled"))
|
|
328
|
+
return "cancelled";
|
|
329
|
+
if (states.some((state) => state === "running"))
|
|
330
|
+
return "running";
|
|
331
|
+
return "queued";
|
|
332
|
+
}
|
|
333
|
+
export function buildWorkflowPhaseTree(model) {
|
|
334
|
+
const drafts = new Map();
|
|
335
|
+
const roots = [];
|
|
336
|
+
const add = (node, parentId) => {
|
|
337
|
+
const existing = drafts.get(node.id);
|
|
338
|
+
if (existing)
|
|
339
|
+
return existing;
|
|
340
|
+
const draft = { ...node, ...(parentId === undefined ? {} : { parentId }), children: [] };
|
|
341
|
+
drafts.set(draft.id, draft);
|
|
342
|
+
if (parentId === undefined)
|
|
343
|
+
roots.push(draft.id);
|
|
344
|
+
else
|
|
345
|
+
drafts.get(parentId)?.children.push(draft.id);
|
|
346
|
+
return draft;
|
|
347
|
+
};
|
|
348
|
+
const samePath = (left, right) => left.length === right.length && left.every((part, index) => part === right[index]);
|
|
349
|
+
const addPhase = (phaseId, label, agents, phase) => {
|
|
350
|
+
const phaseNode = add({ id: workflowPhaseTreePath("phase", phaseId, []), kind: "phase", label, depth: 0, phaseId, operationPath: [], state: phase?.state ?? workflowPhaseTreeAggregateState(agents.map((agent) => agent.state)), ...(phase ? { phase } : {}) });
|
|
351
|
+
const operationNodes = new Map();
|
|
352
|
+
const entries = agents.map((agent) => ({ agent, path: [...(agent.structuralPath ?? [])], node: undefined, defaultParentId: phaseNode.id }));
|
|
353
|
+
const agentEntries = new Map(entries.map((entry) => [entry.agent.id, entry]));
|
|
354
|
+
const acceptedParents = new Map();
|
|
355
|
+
const wouldCycle = (childId, parentId) => {
|
|
356
|
+
const seen = new Set([childId]);
|
|
357
|
+
let current = parentId;
|
|
358
|
+
while (current) {
|
|
359
|
+
if (seen.has(current))
|
|
360
|
+
return true;
|
|
361
|
+
seen.add(current);
|
|
362
|
+
current = acceptedParents.get(current);
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
};
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
const parent = entry.agent.parentId ? agentEntries.get(entry.agent.parentId) : undefined;
|
|
368
|
+
if (parent && !wouldCycle(entry.agent.id, parent.agent.id))
|
|
369
|
+
acceptedParents.set(entry.agent.id, parent.agent.id);
|
|
370
|
+
}
|
|
371
|
+
const operationChain = (path, owner, startIndex = 0) => {
|
|
372
|
+
let parent = owner;
|
|
373
|
+
for (let index = startIndex; index < path.length; index += 1) {
|
|
374
|
+
const prefix = path.slice(0, index + 1);
|
|
375
|
+
const key = `${owner.id}:${JSON.stringify(prefix)}`;
|
|
376
|
+
const existing = operationNodes.get(key);
|
|
377
|
+
if (existing) {
|
|
378
|
+
parent = existing;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const suffix = path.slice(startIndex, index + 1).map((part) => encodeURIComponent(part)).join("/");
|
|
382
|
+
const id = owner.id === phaseNode.id ? workflowPhaseTreePath("operation", phaseId, prefix) : `${owner.id}/operation/${suffix}`;
|
|
383
|
+
const operation = add({ id, kind: "operation", label: prefix.at(-1) ?? "", depth: 0, phaseId, operationPath: prefix, state: "queued", ...(phase ? { phase } : {}) }, parent.id);
|
|
384
|
+
operationNodes.set(key, operation);
|
|
385
|
+
parent = operation;
|
|
386
|
+
}
|
|
387
|
+
return parent;
|
|
388
|
+
};
|
|
389
|
+
for (const entry of entries) {
|
|
390
|
+
if (!acceptedParents.has(entry.agent.id))
|
|
391
|
+
entry.defaultParentId = operationChain(entry.path, phaseNode).id;
|
|
392
|
+
}
|
|
393
|
+
for (const entry of entries) {
|
|
394
|
+
entry.node = add({ id: workflowPhaseTreePath("agent", phaseId, entry.path, entry.agent.id), kind: "agent", label: entry.agent.label ?? entry.agent.name, depth: 0, phaseId, operationPath: entry.path, state: entry.agent.state, agentId: entry.agent.id, agent: entry.agent }, phaseNode.id);
|
|
395
|
+
}
|
|
396
|
+
const attach = (entry, parentId) => {
|
|
397
|
+
const previous = entry.node.parentId ? drafts.get(entry.node.parentId) : undefined;
|
|
398
|
+
if (previous)
|
|
399
|
+
previous.children = previous.children.filter((childId) => childId !== entry.node.id);
|
|
400
|
+
entry.node.parentId = parentId;
|
|
401
|
+
const parent = drafts.get(parentId);
|
|
402
|
+
if (parent && !parent.children.includes(entry.node.id))
|
|
403
|
+
parent.children.push(entry.node.id);
|
|
404
|
+
};
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
const parentId = acceptedParents.get(entry.agent.id);
|
|
407
|
+
const parent = parentId ? agentEntries.get(parentId) : undefined;
|
|
408
|
+
if (parent) {
|
|
409
|
+
if (samePath(entry.path, parent.path))
|
|
410
|
+
entry.defaultParentId = parent.node.id;
|
|
411
|
+
else {
|
|
412
|
+
const commonLength = entry.path.findIndex((part, index) => parent.path[index] !== part);
|
|
413
|
+
const startIndex = commonLength < 0 ? Math.min(entry.path.length, parent.path.length) : commonLength;
|
|
414
|
+
entry.defaultParentId = operationChain(entry.path, parent.node, startIndex).id;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
attach(entry, entry.defaultParentId);
|
|
418
|
+
}
|
|
419
|
+
const setDepth = (node, depth, seen = new Set()) => {
|
|
420
|
+
if (seen.has(node.id))
|
|
421
|
+
return;
|
|
422
|
+
seen.add(node.id);
|
|
423
|
+
node.depth = depth;
|
|
424
|
+
for (const childId of node.children) {
|
|
425
|
+
const child = drafts.get(childId);
|
|
426
|
+
if (child)
|
|
427
|
+
setDepth(child, depth + 1, seen);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
setDepth(phaseNode, 0);
|
|
431
|
+
const statesFor = (node, seen = new Set()) => {
|
|
432
|
+
if (seen.has(node.id))
|
|
433
|
+
return [];
|
|
434
|
+
const nextSeen = new Set(seen).add(node.id);
|
|
435
|
+
return node.children.flatMap((childId) => {
|
|
436
|
+
const child = drafts.get(childId);
|
|
437
|
+
return child?.kind === "agent" ? [child.agent?.state ?? "queued", ...statesFor(child, nextSeen)] : child ? statesFor(child, nextSeen) : [];
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
for (const operation of operationNodes.values())
|
|
441
|
+
operation.state = workflowPhaseTreeAggregateState(statesFor(operation));
|
|
442
|
+
};
|
|
443
|
+
for (const phase of model.phases)
|
|
444
|
+
addPhase(phase.id, `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`, phase.agents, phase);
|
|
445
|
+
if (model.unassignedAgents?.length)
|
|
446
|
+
addPhase("unassigned", "Unassigned", model.unassignedAgents);
|
|
447
|
+
const nodes = [...drafts.values()].map((node) => ({ ...node, children: [...node.children] }));
|
|
448
|
+
return { roots, nodes, byId: new Map(nodes.map((node) => [node.id, node])) };
|
|
449
|
+
}
|
|
450
|
+
export function workflowPhaseTreeVisibleNodes(tree, expanded = new Set()) {
|
|
451
|
+
const visible = [];
|
|
452
|
+
const visit = (id) => {
|
|
453
|
+
const node = tree.byId.get(id);
|
|
454
|
+
if (!node)
|
|
455
|
+
return;
|
|
456
|
+
visible.push(node);
|
|
457
|
+
if (expanded.has(node.id))
|
|
458
|
+
for (const childId of node.children)
|
|
459
|
+
visit(childId);
|
|
460
|
+
};
|
|
461
|
+
for (const root of tree.roots)
|
|
462
|
+
visit(root);
|
|
463
|
+
return visible;
|
|
464
|
+
}
|
|
465
|
+
export function workflowPhaseTreeInitialExpanded(tree) {
|
|
466
|
+
return new Set(tree.nodes.filter((node) => node.children.length > 0).map((node) => node.id));
|
|
467
|
+
}
|
|
468
|
+
export function preserveWorkflowPhaseTreeSelection(tree, selection) {
|
|
469
|
+
const node = (selection.nodeId ? tree.byId.get(selection.nodeId) : undefined) ?? tree.nodes[0];
|
|
470
|
+
return node ? { nodeId: node.id } : {};
|
|
471
|
+
}
|
|
472
|
+
export function navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, direction) {
|
|
473
|
+
const expanded = new Set(expandedNodeIds);
|
|
474
|
+
const current = (selectedNodeId ? tree.byId.get(selectedNodeId) : undefined) ?? tree.nodes[0];
|
|
475
|
+
if (!current)
|
|
476
|
+
return { expandedNodeIds: expanded };
|
|
477
|
+
if (direction === "left") {
|
|
478
|
+
if (current.children.length && expanded.delete(current.id))
|
|
479
|
+
return { nodeId: current.id, expandedNodeIds: expanded };
|
|
480
|
+
return { nodeId: current.parentId ?? current.id, expandedNodeIds: expanded };
|
|
481
|
+
}
|
|
482
|
+
if (direction === "right") {
|
|
483
|
+
if (current.children.length && !expanded.has(current.id)) {
|
|
484
|
+
expanded.add(current.id);
|
|
485
|
+
return { nodeId: current.id, expandedNodeIds: expanded };
|
|
486
|
+
}
|
|
487
|
+
return { nodeId: current.children[0] ?? current.id, expandedNodeIds: expanded };
|
|
488
|
+
}
|
|
489
|
+
const visible = workflowPhaseTreeVisibleNodes(tree, expanded);
|
|
490
|
+
const index = Math.max(0, visible.findIndex((node) => node.id === current.id));
|
|
491
|
+
const next = visible[(index + (direction === "up" ? visible.length - 1 : 1)) % visible.length];
|
|
492
|
+
return { nodeId: next?.id ?? current.id, expandedNodeIds: expanded };
|
|
493
|
+
}
|
|
285
494
|
export function preserveWorkflowPhaseSelection(model, selection) {
|
|
286
495
|
const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
|
|
287
496
|
if (!phase)
|
|
288
|
-
return {};
|
|
289
|
-
const
|
|
290
|
-
|
|
497
|
+
return model.unassignedAgents?.length ? { nodeId: workflowPhaseTreePath("phase", "unassigned", []) } : {};
|
|
498
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
499
|
+
const selectedAgent = selection.agentId ? phase.agents.find((candidate) => candidate.id === selection.agentId) : undefined;
|
|
500
|
+
const selectedCandidate = selection.nodeId ? tree.byId.get(selection.nodeId) : undefined;
|
|
501
|
+
const selected = selectedCandidate?.phaseId === phase.id ? selectedCandidate : selectedAgent ? tree.byId.get(workflowPhaseTreePath("agent", phase.id, selectedAgent.structuralPath ?? [], selectedAgent.id)) : undefined;
|
|
502
|
+
const nodeId = selected?.id ?? tree.byId.get(workflowPhaseTreePath("phase", phase.id, []))?.id;
|
|
503
|
+
return { phaseId: phase.id, ...(selection.agentId && phase.agents.some((agent) => agent.id === selection.agentId) ? { agentId: selection.agentId } : phase.agents[0] ? { agentId: phase.agents[0].id } : {}), ...(nodeId ? { nodeId } : {}), ...(selection.expandedNodeIds ? { expandedNodeIds: selection.expandedNodeIds } : {}) };
|
|
291
504
|
}
|
|
292
505
|
function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
293
506
|
function agentGroupLabel(agents) {
|
|
@@ -322,28 +535,36 @@ function renderGroupedAgents(agents, render, allAgents = agents, groupLabel = (l
|
|
|
322
535
|
...group.entries.map((entry) => render(entry, grouped)),
|
|
323
536
|
]);
|
|
324
537
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
538
|
+
const RUN_STATE_GLYPH = { completed: "✓", failed: "✗", stopped: "✗", budget_exhausted: "!", awaiting_input: "●" };
|
|
539
|
+
const AGENT_STATE_GLYPH = { completed: "✓", failed: "✗", cancelled: "✗" };
|
|
540
|
+
function runStateGlyph(state, running) { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
541
|
+
function agentStateGlyph(state, running) { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
|
|
542
|
+
const PROGRESS_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", running: "accent" };
|
|
543
|
+
const WORKFLOW_ICON_STYLE = { completed: "success", failed: "error", stopped: "error", budget_exhausted: "warning", running: "accent" };
|
|
544
|
+
const PHASE_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", running: "accent", interrupted: "warning", budget_exhausted: "warning" };
|
|
545
|
+
function styleForState(map, state, styles) {
|
|
546
|
+
const key = map[state] ?? "muted";
|
|
547
|
+
return (text) => styles[key](text);
|
|
548
|
+
}
|
|
549
|
+
function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
550
|
+
function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
551
|
+
function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
552
|
+
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
335
553
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
336
|
-
const workflowIcon = run.state
|
|
337
|
-
const
|
|
554
|
+
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
555
|
+
const iconStyle = workflowIconStyle(run.state, styles);
|
|
338
556
|
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
339
|
-
const lines = [`${
|
|
557
|
+
const lines = [`${iconStyle(workflowIcon)} ${header}`];
|
|
340
558
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
341
559
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
560
|
+
const activeShells = run.activeShells ?? 0;
|
|
561
|
+
if (activeShells > 0)
|
|
562
|
+
lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
|
|
342
563
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
343
564
|
const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
344
|
-
const icon = agent.state
|
|
565
|
+
const icon = agentStateGlyph(agent.state, spinner);
|
|
345
566
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
346
|
-
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
567
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
347
568
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
348
569
|
const state = progressStyleForState(agent.state, styles);
|
|
349
570
|
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
|
|
@@ -392,6 +613,104 @@ function workflowCatalogBlock(text, expanded) {
|
|
|
392
613
|
invalidate() { },
|
|
393
614
|
};
|
|
394
615
|
}
|
|
616
|
+
function controlString(value) { return typeof value === "string" && value.trim() ? value : undefined; }
|
|
617
|
+
function controlValue(value) {
|
|
618
|
+
if (value === null)
|
|
619
|
+
return "removed";
|
|
620
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
621
|
+
return String(value);
|
|
622
|
+
const json = JSON.stringify(value);
|
|
623
|
+
return typeof json === "string" ? json : "unknown";
|
|
624
|
+
}
|
|
625
|
+
function controlTitle(name, theme) { return theme.fg("toolTitle", theme.bold(name)); }
|
|
626
|
+
function controlState(state, theme) {
|
|
627
|
+
const color = state === "completed" || state === "running" || state === "stopped" ? "success" : state === "failed" || state === "unknown" ? "error" : state === "budget_exhausted" || state === "awaiting_approval" ? "warning" : "accent";
|
|
628
|
+
return theme.fg(color, state);
|
|
629
|
+
}
|
|
630
|
+
function controlAction(action, theme) {
|
|
631
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
632
|
+
return theme.fg(color, action);
|
|
633
|
+
}
|
|
634
|
+
function budgetPatchEntries(value) {
|
|
635
|
+
if (!object(value))
|
|
636
|
+
return value === undefined ? [] : [controlValue(value)];
|
|
637
|
+
return Object.entries(value).map(([dimension, limits]) => {
|
|
638
|
+
if (limits === null)
|
|
639
|
+
return `${dimension}=removed`;
|
|
640
|
+
if (!object(limits))
|
|
641
|
+
return `${dimension}=${controlValue(limits)}`;
|
|
642
|
+
const parts = ["soft", "hard"].filter((key) => Object.prototype.hasOwnProperty.call(limits, key)).map((key) => `${key}=${controlValue(limits[key])}`);
|
|
643
|
+
return `${dimension} ${parts.join(" ")}`;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function budgetPatchSummary(value) {
|
|
647
|
+
const entries = budgetPatchEntries(value);
|
|
648
|
+
return entries.length ? entries.join(", ") : "unchanged";
|
|
649
|
+
}
|
|
650
|
+
function budgetPatchDetails(value, theme) {
|
|
651
|
+
const entries = budgetPatchEntries(value);
|
|
652
|
+
return entries.length ? [theme.fg("accent", theme.bold("Budget patch")), ...entries.map((entry) => ` ${theme.fg("toolOutput", entry)}`)] : [];
|
|
653
|
+
}
|
|
654
|
+
function workflowControlValue(result) { return catalogResultValue(result); }
|
|
655
|
+
function workflowControlCall(name, args, theme) {
|
|
656
|
+
const runId = controlString(args.runId) ?? "(missing run ID)";
|
|
657
|
+
if (name === "workflow_respond") {
|
|
658
|
+
const proposalId = controlString(args.proposalId);
|
|
659
|
+
const target = proposalId ? `budget proposal ${proposalId}` : `checkpoint ${controlString(args.name) ?? "(missing name)"}`;
|
|
660
|
+
const decision = args.approved === true ? "approve" : "reject";
|
|
661
|
+
return [`${controlTitle(name, theme)} ${theme.fg("accent", runId)}`, `${theme.fg("muted", target)} · ${controlAction(decision, theme)}`].join("\n");
|
|
662
|
+
}
|
|
663
|
+
if (name === "workflow_resume")
|
|
664
|
+
return args.budget === undefined ? `${controlTitle(name, theme)} ${theme.fg("accent", runId)}` : [`${controlTitle(name, theme)} ${theme.fg("accent", runId)}`, `${theme.fg("muted", "Budget")} ${theme.fg("toolOutput", budgetPatchSummary(args.budget))}`].join("\n");
|
|
665
|
+
if (name === "workflow_retry")
|
|
666
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)} ${theme.fg("muted", "failed run")}`;
|
|
667
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)}`;
|
|
668
|
+
}
|
|
669
|
+
function workflowControlResult(name, args, result, expanded, theme, isError) {
|
|
670
|
+
if (isError) {
|
|
671
|
+
const text = result.content?.filter(({ type }) => type === "text").map(({ text }) => text ?? "").join("\n").trim();
|
|
672
|
+
return theme.fg("error", text || `The ${name} tool failed.`);
|
|
673
|
+
}
|
|
674
|
+
const value = workflowControlValue(result);
|
|
675
|
+
if (!object(value))
|
|
676
|
+
return theme.fg("error", `The ${name} tool returned an invalid result.`);
|
|
677
|
+
const runId = controlString(args.runId) ?? controlString(value.runId) ?? "(unknown)";
|
|
678
|
+
const title = controlTitle(name, theme);
|
|
679
|
+
if (name === "workflow_stop") {
|
|
680
|
+
const state = controlString(value.state) ?? "unknown";
|
|
681
|
+
const action = value.stopped === true ? "stopped" : value.reason === "already_terminal" ? "already terminal" : value.reason === "unknown_run" ? "run not found" : "no change";
|
|
682
|
+
if (!expanded)
|
|
683
|
+
return `${title}\nRun ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`;
|
|
684
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(controlString(value.reason) ? [`Reason: ${theme.fg("toolOutput", controlValue(value.reason))}`] : [])].join("\n");
|
|
685
|
+
}
|
|
686
|
+
if (name === "workflow_retry") {
|
|
687
|
+
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
688
|
+
const state = controlString(value.state) ?? "unknown";
|
|
689
|
+
const action = state === "completed" ? "completed" : "started";
|
|
690
|
+
if (!expanded)
|
|
691
|
+
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
692
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
|
|
693
|
+
}
|
|
694
|
+
if (name === "workflow_resume") {
|
|
695
|
+
const state = controlString(value.state) ?? "unknown";
|
|
696
|
+
const proposalId = controlString(value.proposalId);
|
|
697
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
698
|
+
if (!expanded)
|
|
699
|
+
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
700
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal: ${theme.fg("accent", proposalId)}`] : []), ...budgetPatchDetails(args.budget, theme)].join("\n");
|
|
701
|
+
}
|
|
702
|
+
const proposalId = controlString(args.proposalId);
|
|
703
|
+
const checkpointName = controlString(args.name);
|
|
704
|
+
const target = proposalId ? `Budget proposal ${theme.fg("accent", proposalId)}` : `Checkpoint ${theme.fg("accent", checkpointName ?? "(missing)")}`;
|
|
705
|
+
const accepted = value.accepted === true;
|
|
706
|
+
const approved = value.approved === true;
|
|
707
|
+
const reason = controlString(value.reason);
|
|
708
|
+
const action = reason === "proposal_not_pending" ? "not pending" : reason === "checkpoint" && !accepted ? "not pending" : approved ? "approved" : "rejected";
|
|
709
|
+
const state = controlString(value.state);
|
|
710
|
+
if (!expanded)
|
|
711
|
+
return [title, target, `Run ${theme.fg("accent", runId)} · ${controlAction(action, theme)}${state ? ` · ${controlState(state, theme)}` : ""}`].join("\n");
|
|
712
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `Target: ${target}`, `Action: ${controlAction(action, theme)}`, ...(state ? [`State: ${controlState(state, theme)}`] : []), ...(reason ? [`Reason: ${theme.fg("toolOutput", reason)}`] : [])].join("\n");
|
|
713
|
+
}
|
|
395
714
|
function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
|
|
396
715
|
function catalogResultValue(result) {
|
|
397
716
|
if (result.details !== undefined)
|
|
@@ -468,7 +787,8 @@ function formatWorkflowCatalog(value, expanded, theme) {
|
|
|
468
787
|
return theme.fg("error", value.error.message);
|
|
469
788
|
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
470
789
|
}
|
|
471
|
-
const
|
|
790
|
+
const ANSI_SGR_SOURCE = `${String.fromCharCode(27)}\\[[0-9;]*m`;
|
|
791
|
+
const ANSI_SGR = new RegExp(ANSI_SGR_SOURCE);
|
|
472
792
|
export function truncateWorkflowProgress(text, width) {
|
|
473
793
|
const safeWidth = Math.max(1, width);
|
|
474
794
|
return text.split("\n").flatMap((line) => {
|
|
@@ -539,7 +859,7 @@ function navigatorRunLabels(entries) {
|
|
|
539
859
|
nameCount.set(run.workflowName, (nameCount.get(run.workflowName) ?? 0) + 1);
|
|
540
860
|
return entries.map(({ store, loaded: { run } }) => {
|
|
541
861
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
542
|
-
const glyph = run.state
|
|
862
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
543
863
|
const suffix = (nameCount.get(run.workflowName) ?? 0) > 1 ? ` ${store.runId.slice(0, 8)}` : "";
|
|
544
864
|
const cost = run.agents.reduce((sum, a) => sum + (a.accounting?.cost ?? 0), 0);
|
|
545
865
|
const costStr = cost > 0 ? ` $${cost.toFixed(2)}` : "";
|
|
@@ -574,20 +894,46 @@ function styledAgentBreadcrumb(agent, byId, styles) {
|
|
|
574
894
|
return parts[0] ?? "";
|
|
575
895
|
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
576
896
|
}
|
|
577
|
-
function
|
|
897
|
+
export function formatStalledDuration(durationMs) {
|
|
898
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
899
|
+
if (minutes < 60)
|
|
900
|
+
return `${String(minutes)}m`;
|
|
901
|
+
const hours = Math.floor(minutes / 60);
|
|
902
|
+
const remainingMinutes = minutes % 60;
|
|
903
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
904
|
+
}
|
|
905
|
+
function stalledDuration(agent, now) {
|
|
906
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
|
|
907
|
+
return undefined;
|
|
908
|
+
const duration = now - agent.lastEventAt;
|
|
909
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
910
|
+
}
|
|
911
|
+
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
578
912
|
const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
|
|
579
|
-
|
|
913
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
914
|
+
const stalled = stalledDuration(agent, now);
|
|
915
|
+
if (stalled === undefined)
|
|
916
|
+
return activity;
|
|
917
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
918
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
919
|
+
}
|
|
920
|
+
function formatAccountingValue(value) {
|
|
921
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
580
922
|
}
|
|
581
923
|
function formatAccounting(accounting) {
|
|
582
924
|
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
583
|
-
return `${
|
|
925
|
+
return `${formatAccountingValue(total)} tok`;
|
|
584
926
|
}
|
|
585
|
-
|
|
927
|
+
function formatAgentAccounting(accounting) {
|
|
928
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
929
|
+
return [`Tokens: ${formatAccountingValue(total)} (in=${formatAccountingValue(accounting.input)} out=${formatAccountingValue(accounting.output)} cache-read=${formatAccountingValue(accounting.cacheRead)} cache-write=${formatAccountingValue(accounting.cacheWrite)})`, `Cost: $${accounting.cost.toFixed(2)}`];
|
|
930
|
+
}
|
|
931
|
+
export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
|
|
586
932
|
void worktrees;
|
|
587
933
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
588
934
|
const totalAccounting = run.agents.reduce((sum, a) => ({ input: sum.input + (a.accounting?.input ?? 0), output: sum.output + (a.accounting?.output ?? 0), cacheRead: sum.cacheRead + (a.accounting?.cacheRead ?? 0), cacheWrite: sum.cacheWrite + (a.accounting?.cacheWrite ?? 0), cost: sum.cost + (a.accounting?.cost ?? 0) }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
589
935
|
const hasAccounting = run.agents.some((a) => a.accounting);
|
|
590
|
-
const glyph = run.state
|
|
936
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
591
937
|
const header = `${glyph} ${run.workflowName}`;
|
|
592
938
|
const meta = [run.state, run.phase ? `phase: ${run.phase}` : "", `${String(done)}/${String(run.agents.length)} agents`, hasAccounting ? formatAccounting(totalAccounting) : "", totalAccounting.cost > 0 ? `$${totalAccounting.cost.toFixed(2)}` : ""].filter(Boolean).join(" · ");
|
|
593
939
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
@@ -598,7 +944,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
598
944
|
lines.push("");
|
|
599
945
|
const byId = new Map(run.agents.map((a) => [a.id, a]));
|
|
600
946
|
const render = ({ agent, depth }, grouped) => {
|
|
601
|
-
const icon = agent.state
|
|
947
|
+
const icon = agentStateGlyph(agent.state, "⠦");
|
|
602
948
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
603
949
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
604
950
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
@@ -608,7 +954,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
608
954
|
if (last?.error)
|
|
609
955
|
result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
610
956
|
}
|
|
611
|
-
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
|
|
957
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
612
958
|
if (activity)
|
|
613
959
|
result.push(`${indent} ${activity}`);
|
|
614
960
|
return result.join("\n");
|
|
@@ -621,7 +967,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
621
967
|
}
|
|
622
968
|
return lines.join("\n");
|
|
623
969
|
}
|
|
624
|
-
export function formatNavigatorRun(loaded, checkpoints,
|
|
970
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
|
|
625
971
|
const { run, snapshot } = loaded;
|
|
626
972
|
const lines = [
|
|
627
973
|
`Workflow: ${run.workflowName}`,
|
|
@@ -657,6 +1003,9 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
657
1003
|
result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
658
1004
|
for (const call of agent.toolCalls ?? [])
|
|
659
1005
|
result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
1006
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
1007
|
+
if (activity)
|
|
1008
|
+
result.push(`${indent} ${activity}`);
|
|
660
1009
|
return result.join("\n");
|
|
661
1010
|
}));
|
|
662
1011
|
lines.push("Checkpoints:");
|
|
@@ -664,23 +1013,83 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
664
1013
|
lines.push(" (none)");
|
|
665
1014
|
for (const checkpoint of checkpoints)
|
|
666
1015
|
lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
667
|
-
lines.push(`Worktrees: ${String(
|
|
1016
|
+
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
668
1017
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
669
1018
|
return lines.join("\n");
|
|
670
1019
|
}
|
|
671
|
-
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
1020
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
672
1021
|
const safeWidth = Math.max(1, width);
|
|
673
1022
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
1023
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
1024
|
+
const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
|
|
674
1025
|
const wrap = (text, limit = safeWidth) => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
const
|
|
1026
|
+
// ponytail: ANSI-only width, good enough for the ASCII labels the tree renders
|
|
1027
|
+
const ansiPattern = new RegExp(ANSI_SGR_SOURCE, "g");
|
|
1028
|
+
const visibleLength = (text) => text.replace(ansiPattern, "").length;
|
|
1029
|
+
const padTo = (text, limit) => `${text}${" ".repeat(Math.max(0, limit - visibleLength(text)))}`;
|
|
1030
|
+
const phaseStyle = (state) => phaseStyleForState(state, styles);
|
|
1031
|
+
const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
|
|
1032
|
+
const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
|
|
1033
|
+
const selectedNode = (selection.nodeId ? tree.byId.get(selection.nodeId) : undefined) ?? selectedByAgent ?? (phase ? tree.byId.get(workflowPhaseTreePath("phase", phase.id, [])) : undefined) ?? (model.currentPhaseId ? tree.byId.get(workflowPhaseTreePath("phase", model.currentPhaseId, [])) : undefined) ?? tree.nodes[0];
|
|
1034
|
+
const selectedPhase = selectedNode?.phase ?? (selectedNode ? model.phases.find((candidate) => candidate.id === selectedNode.phaseId) : undefined);
|
|
1035
|
+
const visibleNodes = workflowPhaseTreeVisibleNodes(tree, expanded);
|
|
1036
|
+
const nodeAgents = (node) => {
|
|
1037
|
+
const agents = [];
|
|
1038
|
+
const visit = (id) => {
|
|
1039
|
+
const child = tree.byId.get(id);
|
|
1040
|
+
if (!child)
|
|
1041
|
+
return;
|
|
1042
|
+
if (child.agent)
|
|
1043
|
+
agents.push(child.agent);
|
|
1044
|
+
else
|
|
1045
|
+
for (const childId of child.children)
|
|
1046
|
+
visit(childId);
|
|
1047
|
+
};
|
|
1048
|
+
if (node.agent)
|
|
1049
|
+
agents.push(node.agent);
|
|
1050
|
+
else
|
|
1051
|
+
for (const childId of node.children)
|
|
1052
|
+
visit(childId);
|
|
1053
|
+
return agents;
|
|
1054
|
+
};
|
|
1055
|
+
const nodeStatus = (node) => phaseStyle(node.state)(node.state);
|
|
1056
|
+
const nodeIcon = (node) => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : " ";
|
|
1057
|
+
const treeLine = (node) => {
|
|
1058
|
+
const selected = node.id === selectedNode?.id;
|
|
1059
|
+
const state = progressStyleForState(node.state, styles);
|
|
1060
|
+
const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
|
|
1061
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
|
|
1062
|
+
};
|
|
1063
|
+
const details = (node) => {
|
|
1064
|
+
if (!node)
|
|
1065
|
+
return [styles.muted("No workflow node is selected")];
|
|
1066
|
+
const agents = nodeAgents(node);
|
|
1067
|
+
if (node.kind === "phase") {
|
|
1068
|
+
const selected = node.phase;
|
|
1069
|
+
const counts = selected?.counts ?? phaseAgentCounts(agents);
|
|
1070
|
+
return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`];
|
|
1071
|
+
}
|
|
1072
|
+
if (node.kind === "operation") {
|
|
1073
|
+
const states = phaseAgentCounts(agents);
|
|
1074
|
+
return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`];
|
|
1075
|
+
}
|
|
1076
|
+
const agent = node.agent;
|
|
1077
|
+
if (!agent)
|
|
1078
|
+
return [styles.muted("Agent details are unavailable")];
|
|
1079
|
+
const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
|
|
1080
|
+
const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
1081
|
+
const error = agent.attemptDetails?.at(-1)?.error;
|
|
1082
|
+
if (error)
|
|
1083
|
+
result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
1084
|
+
if (agent.activity)
|
|
1085
|
+
result.push(`Activity: ${agent.activity.text}`);
|
|
1086
|
+
const stalled = stalledDuration(agent, now);
|
|
1087
|
+
if (stalled !== undefined)
|
|
1088
|
+
result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
|
|
1089
|
+
return result;
|
|
1090
|
+
};
|
|
679
1091
|
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
680
1092
|
const statusSummary = stateNames.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
|
|
681
|
-
const selectedIndex = selection.phaseId ? model.phases.findIndex((phase) => phase.id === selection.phaseId) : -1;
|
|
682
|
-
const activeIndex = selectedIndex >= 0 ? selectedIndex : model.currentPhaseIndex ?? (model.phases.length ? 0 : -1);
|
|
683
|
-
const selectedPhase = activeIndex >= 0 ? model.phases[activeIndex] : undefined;
|
|
684
1093
|
const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
685
1094
|
if (run.error)
|
|
686
1095
|
lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
@@ -688,46 +1097,32 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
688
1097
|
for (const event of run.events ?? [])
|
|
689
1098
|
lines.push(styles.warning(`Warning: ${event.message}`));
|
|
690
1099
|
lines.push(...formatCompactBudgetStatus(run));
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const selected = agent.id === selectedAgentId;
|
|
698
|
-
const stateStyle = progressStyleForState(agent.state, styles);
|
|
699
|
-
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
700
|
-
rendered.push(`${selected ? "→" : " "} ${stateStyle(icon)} ${styledAgentBreadcrumb(agent, byId, styles)} · ${stateStyle(agent.state)}`);
|
|
701
|
-
if (agent.state === "failed") {
|
|
702
|
-
const error = agent.attemptDetails?.at(-1)?.error;
|
|
703
|
-
if (error)
|
|
704
|
-
rendered.push(styles.error(` error: ${error.code}: ${error.message}`));
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return rendered;
|
|
1100
|
+
const renderTree = (limit) => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
|
|
1101
|
+
const actionRows = () => {
|
|
1102
|
+
const actions = selection.actions;
|
|
1103
|
+
if (!actions)
|
|
1104
|
+
return [];
|
|
1105
|
+
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
708
1106
|
};
|
|
709
|
-
|
|
710
|
-
lines.push(styles.muted("Phases: no declared or observed phases (legacy snapshot)"), styles.bold("Agents"), ...renderAgentLines(run.agents, selection.agentId));
|
|
711
|
-
return lines.flatMap((line) => wrap(line));
|
|
712
|
-
}
|
|
713
|
-
const selectedAgent = selectedPhase?.agents.find((agent) => agent.id === selection.agentId) ?? selectedPhase?.agents[0];
|
|
1107
|
+
const detailRows = () => [...details(selectedNode), ...actionRows()];
|
|
714
1108
|
if (safeWidth >= 80) {
|
|
715
|
-
const sidebarWidth = Math.min(
|
|
1109
|
+
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
716
1110
|
const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
|
|
717
|
-
const sidebar =
|
|
718
|
-
const detail =
|
|
1111
|
+
const sidebar = renderTree(sidebarWidth).flatMap((line) => wrap(line, sidebarWidth));
|
|
1112
|
+
const detail = detailRows().flatMap((line) => wrap(line, detailWidth));
|
|
719
1113
|
const rows = Math.max(sidebar.length, detail.length);
|
|
720
1114
|
for (let index = 0; index < rows; index += 1)
|
|
721
|
-
lines.push(`${sidebar[index] ?? ""} | ${detail[index] ?? ""}`);
|
|
1115
|
+
lines.push(`${padTo(sidebar[index] ?? "", sidebarWidth)} | ${detail[index] ?? ""}`);
|
|
1116
|
+
}
|
|
1117
|
+
else if (selection.detailsOnly) {
|
|
1118
|
+
lines.push(...detailRows().flatMap((line) => wrap(line)));
|
|
722
1119
|
}
|
|
723
1120
|
else {
|
|
724
|
-
lines.push(
|
|
725
|
-
|
|
726
|
-
lines.push(...
|
|
727
|
-
if (selectedPhase)
|
|
728
|
-
lines.push("", styles.bold(`Selected phase: ${phaseName(selectedPhase)}`), ...wrap(`Status: ${phaseStyle(selectedPhase.state)(selectedPhase.state)}`), ...wrap(phaseCounts(selectedPhase)), styles.bold("Agents"), ...renderAgentLines(selectedPhase.agents, selectedAgent?.id).flatMap((line) => wrap(line)));
|
|
1121
|
+
lines.push(...renderTree(safeWidth));
|
|
1122
|
+
if (!selection.treeOnly)
|
|
1123
|
+
lines.push("", ...detailRows().flatMap((line) => wrap(line)));
|
|
729
1124
|
}
|
|
730
|
-
if (model.unassignedAgents?.length)
|
|
1125
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
|
|
731
1126
|
lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
732
1127
|
return lines.flatMap((line) => wrap(line));
|
|
733
1128
|
}
|
|
@@ -932,15 +1327,35 @@ export function formatWorkflowFailureDiagnostics(diagnostic) {
|
|
|
932
1327
|
const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
|
|
933
1328
|
return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
|
|
934
1329
|
}
|
|
1330
|
+
function deliveryPart(value, maxBytes) { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
|
|
1331
|
+
export function formatWorkflowFailureDelivery(diagnostic) {
|
|
1332
|
+
const name = deliveryPart(diagnostic.workflowName, 128);
|
|
1333
|
+
const runId = deliveryPart(diagnostic.runId, 128);
|
|
1334
|
+
const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
|
|
1335
|
+
const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
|
|
1336
|
+
const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
|
|
1337
|
+
const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
|
|
1338
|
+
const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
|
|
1339
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1340
|
+
}
|
|
1341
|
+
function formatWorkflowFailureDeliveryFallback(workflowName, runId, runDirectory, error) {
|
|
1342
|
+
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
1343
|
+
const failedPath = workflowFailedAt(error);
|
|
1344
|
+
const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
|
|
1345
|
+
const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
|
|
1346
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1347
|
+
}
|
|
935
1348
|
function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
|
|
936
1349
|
function isWorkflowFailureDiagnostics(value) {
|
|
937
1350
|
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
938
1351
|
}
|
|
939
1352
|
function deliver(pi, content) {
|
|
1353
|
+
if (typeof pi.sendMessage !== "function")
|
|
1354
|
+
return;
|
|
940
1355
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
941
1356
|
}
|
|
942
1357
|
function deliverFailure(pi, diagnostic) {
|
|
943
|
-
deliver(pi,
|
|
1358
|
+
deliver(pi, formatWorkflowFailureDelivery(diagnostic));
|
|
944
1359
|
}
|
|
945
1360
|
function safeEventError(error) {
|
|
946
1361
|
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
@@ -1216,23 +1631,22 @@ function projectTrusted(ctx) {
|
|
|
1216
1631
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
1217
1632
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
1218
1633
|
}
|
|
1219
|
-
function
|
|
1634
|
+
function asFn(value) { return typeof value === "function" ? value : undefined; }
|
|
1220
1635
|
function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
|
|
1221
1636
|
function piHostCapabilities(pi) {
|
|
1222
1637
|
if (!object(pi))
|
|
1223
1638
|
return {};
|
|
1224
|
-
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
1639
|
+
const registerEntryRenderer = asFn(pi.registerEntryRenderer);
|
|
1225
1640
|
const events = pi.events;
|
|
1226
|
-
return { ...(
|
|
1641
|
+
return { ...(registerEntryRenderer ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
1227
1642
|
}
|
|
1228
|
-
function isModelRegistryGetter(value) { return typeof value === "function"; }
|
|
1229
1643
|
function contextHostCapabilities(ctx) {
|
|
1230
1644
|
if (!object(ctx) || !object(ctx.modelRegistry))
|
|
1231
1645
|
return {};
|
|
1232
1646
|
const registry = ctx.modelRegistry;
|
|
1233
|
-
const getAll = registry.getAll;
|
|
1234
|
-
const getAvailable = registry.getAvailable;
|
|
1235
|
-
return { modelRegistry: { ...(
|
|
1647
|
+
const getAll = asFn(registry.getAll);
|
|
1648
|
+
const getAvailable = asFn(registry.getAvailable);
|
|
1649
|
+
return { modelRegistry: { ...(getAll ? { getAll: () => getAll.call(registry) } : {}), ...(getAvailable ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
1236
1650
|
}
|
|
1237
1651
|
function modelInventory(root, registry) {
|
|
1238
1652
|
const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
@@ -1266,24 +1680,22 @@ async function resolveLaunchAliases(registry, staticAliases, context, availableM
|
|
|
1266
1680
|
throw error;
|
|
1267
1681
|
}
|
|
1268
1682
|
}
|
|
1269
|
-
function isUiSelect(value) { return typeof value === "function"; }
|
|
1270
|
-
function isUiInput(value) { return typeof value === "function"; }
|
|
1271
|
-
function isUiSetStatus(value) { return typeof value === "function"; }
|
|
1272
1683
|
function uiHostCapabilities(ui) {
|
|
1273
1684
|
if (!object(ui))
|
|
1274
1685
|
return undefined;
|
|
1275
|
-
const select = ui.select;
|
|
1276
|
-
const input = ui.input;
|
|
1277
|
-
const setStatus = ui.setStatus;
|
|
1278
|
-
return { ...(
|
|
1686
|
+
const select = asFn(ui.select);
|
|
1687
|
+
const input = asFn(ui.input);
|
|
1688
|
+
const setStatus = asFn(ui.setStatus);
|
|
1689
|
+
return { ...(select ? { select } : {}), ...(input ? { input } : {}), ...(setStatus ? { setStatus } : {}) };
|
|
1279
1690
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
1691
|
+
function tuiRows(tui) {
|
|
1692
|
+
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1693
|
+
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1284
1694
|
}
|
|
1285
|
-
|
|
1695
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
1286
1696
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1697
|
+
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1698
|
+
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
|
|
1287
1699
|
function borderWorkflowOverlay(component, theme) {
|
|
1288
1700
|
return {
|
|
1289
1701
|
...component,
|
|
@@ -1293,14 +1705,19 @@ function borderWorkflowOverlay(component, theme) {
|
|
|
1293
1705
|
},
|
|
1294
1706
|
};
|
|
1295
1707
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1708
|
+
function keybindingKeys(keybindings, name) {
|
|
1709
|
+
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
|
|
1710
|
+
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1711
|
+
}
|
|
1712
|
+
const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
1713
|
+
function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
1714
|
+
function workflowKeyLabel(keybindings, binding, fallback, labels) {
|
|
1715
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
1716
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
1717
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
1718
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
1301
1719
|
}
|
|
1302
|
-
function
|
|
1303
|
-
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
|
|
1720
|
+
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir, additionalSkillPaths = []) {
|
|
1304
1721
|
beginWorkflowExtensionLoading();
|
|
1305
1722
|
const registry = loadingRegistry();
|
|
1306
1723
|
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
@@ -1353,16 +1770,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1353
1770
|
});
|
|
1354
1771
|
};
|
|
1355
1772
|
const pendingFailureDiagnostics = new Map();
|
|
1356
|
-
|
|
1357
|
-
if (event.toolName !== "workflow" || !event.isError)
|
|
1358
|
-
return;
|
|
1359
|
-
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1360
|
-
if (!diagnostic)
|
|
1361
|
-
return;
|
|
1362
|
-
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1363
|
-
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1364
|
-
});
|
|
1773
|
+
const foregroundDeliveries = new Map();
|
|
1365
1774
|
const liveActivities = new Map();
|
|
1775
|
+
const liveEventTimes = new Map();
|
|
1366
1776
|
const setLiveActivity = (runId, agentId, activity) => {
|
|
1367
1777
|
const activities = liveActivities.get(runId);
|
|
1368
1778
|
if (activity) {
|
|
@@ -1377,12 +1787,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1377
1787
|
liveActivities.delete(runId);
|
|
1378
1788
|
}
|
|
1379
1789
|
};
|
|
1790
|
+
const setLiveEventTime = (runId, agentId, timestamp) => {
|
|
1791
|
+
if (timestamp === undefined)
|
|
1792
|
+
return;
|
|
1793
|
+
const timestamps = liveEventTimes.get(runId);
|
|
1794
|
+
if (timestamps)
|
|
1795
|
+
timestamps.set(agentId, timestamp);
|
|
1796
|
+
else
|
|
1797
|
+
liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
|
|
1798
|
+
};
|
|
1380
1799
|
const withLiveActivities = (run) => {
|
|
1381
1800
|
const activities = liveActivities.get(run.id);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1801
|
+
const timestamps = liveEventTimes.get(run.id);
|
|
1802
|
+
if (!activities?.size && !timestamps?.size)
|
|
1803
|
+
return run;
|
|
1804
|
+
return { ...run, agents: run.agents.map((agent) => {
|
|
1805
|
+
const activity = activities?.get(agent.id);
|
|
1806
|
+
const lastEventAt = timestamps?.get(agent.id);
|
|
1807
|
+
if (activity === undefined && lastEventAt === undefined)
|
|
1808
|
+
return agent;
|
|
1809
|
+
return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1810
|
+
}) };
|
|
1386
1811
|
};
|
|
1387
1812
|
const terminalRunStates = new Map();
|
|
1388
1813
|
let sessionLease;
|
|
@@ -1410,6 +1835,52 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1410
1835
|
await eventPublisher.budget(store, metadata, persisted);
|
|
1411
1836
|
return persisted;
|
|
1412
1837
|
};
|
|
1838
|
+
pi.on("tool_result", async (event) => {
|
|
1839
|
+
const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
|
|
1840
|
+
if (delivery) {
|
|
1841
|
+
if (delivery.timer)
|
|
1842
|
+
clearTimeout(delivery.timer);
|
|
1843
|
+
delivery.inline = true;
|
|
1844
|
+
await delivery.store.updateState((current) => {
|
|
1845
|
+
if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered")
|
|
1846
|
+
return current;
|
|
1847
|
+
return { ...current, delivery: { ...current.delivery, state: "delivered" } };
|
|
1848
|
+
});
|
|
1849
|
+
foregroundDeliveries.delete(event.toolCallId);
|
|
1850
|
+
}
|
|
1851
|
+
if (event.toolName !== "workflow" || !event.isError)
|
|
1852
|
+
return;
|
|
1853
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1854
|
+
if (!diagnostic)
|
|
1855
|
+
return;
|
|
1856
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1857
|
+
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1858
|
+
});
|
|
1859
|
+
const deliverTerminal = async (store, content) => {
|
|
1860
|
+
let claimed;
|
|
1861
|
+
await store.updateState((current) => {
|
|
1862
|
+
if (current.delivery?.state === "delivered")
|
|
1863
|
+
return current;
|
|
1864
|
+
if (!current.delivery) {
|
|
1865
|
+
claimed = true;
|
|
1866
|
+
return current;
|
|
1867
|
+
}
|
|
1868
|
+
claimed = true;
|
|
1869
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
|
|
1870
|
+
});
|
|
1871
|
+
if (claimed === true)
|
|
1872
|
+
deliver(pi, content);
|
|
1873
|
+
};
|
|
1874
|
+
const scheduleForegroundDelivery = (toolCallId, send) => {
|
|
1875
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
1876
|
+
if (!delivery || delivery.inline || typeof pi.sendMessage !== "function")
|
|
1877
|
+
return;
|
|
1878
|
+
//NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
|
|
1879
|
+
delivery.timer = setTimeout(() => {
|
|
1880
|
+
delete delivery.timer;
|
|
1881
|
+
void send().finally(() => foregroundDeliveries.delete(toolCallId));
|
|
1882
|
+
}, 0);
|
|
1883
|
+
};
|
|
1413
1884
|
const phaseBridge = (store, metadata, lifecycle) => {
|
|
1414
1885
|
let cursor = 0;
|
|
1415
1886
|
return async (phase) => {
|
|
@@ -1462,10 +1933,25 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1462
1933
|
const replayed = await store.replay(path);
|
|
1463
1934
|
if (replayed)
|
|
1464
1935
|
return readShellResult(replayed.value);
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1936
|
+
const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
|
|
1937
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
|
|
1938
|
+
try {
|
|
1939
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
1940
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
1941
|
+
await store.complete(path, result);
|
|
1942
|
+
return result;
|
|
1943
|
+
}
|
|
1944
|
+
finally {
|
|
1945
|
+
const stopped = await persistRunState(store, metadata, (current) => {
|
|
1946
|
+
const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
|
|
1947
|
+
if (activeShells > 0)
|
|
1948
|
+
return { ...current, activeShells };
|
|
1949
|
+
const next = { ...current };
|
|
1950
|
+
delete next.activeShells;
|
|
1951
|
+
return next;
|
|
1952
|
+
});
|
|
1953
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
|
|
1954
|
+
}
|
|
1469
1955
|
}
|
|
1470
1956
|
finally {
|
|
1471
1957
|
await lifecycle.leave();
|
|
@@ -1476,8 +1962,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1476
1962
|
budget.transition(next);
|
|
1477
1963
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
1478
1964
|
const nextRun = { ...current, state: next, ...budget.snapshot() };
|
|
1479
|
-
if (next === "running" || next === "completed")
|
|
1965
|
+
if (next === "running" || next === "completed") {
|
|
1480
1966
|
delete nextRun.error;
|
|
1967
|
+
delete nextRun.failedAt;
|
|
1968
|
+
}
|
|
1481
1969
|
return nextRun;
|
|
1482
1970
|
});
|
|
1483
1971
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
@@ -1492,28 +1980,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1492
1980
|
const onProgress = async (progress) => {
|
|
1493
1981
|
let runState;
|
|
1494
1982
|
if (progress.persist) {
|
|
1495
|
-
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) } : current);
|
|
1983
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
|
|
1496
1984
|
}
|
|
1497
1985
|
else {
|
|
1498
1986
|
const loaded = await run.store.load();
|
|
1499
1987
|
if (!loaded.run.agents.some((agent) => agent.id === id))
|
|
1500
1988
|
return;
|
|
1501
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
|
|
1989
|
+
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
|
|
1502
1990
|
}
|
|
1503
1991
|
if (!runState.agents.some((agent) => agent.id === id))
|
|
1504
1992
|
return;
|
|
1505
1993
|
setLiveActivity(runId, id, progress.activity);
|
|
1994
|
+
setLiveEventTime(runId, id, progress.lastEventAt);
|
|
1506
1995
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
1507
1996
|
};
|
|
1508
1997
|
const onAttempt = async (attempt) => {
|
|
1509
1998
|
await scheduler.flush();
|
|
1510
1999
|
scheduler.attemptStarted(id);
|
|
2000
|
+
const lastEventAt = Date.now();
|
|
2001
|
+
setLiveEventTime(runId, id, lastEventAt);
|
|
1511
2002
|
await scheduler.flush();
|
|
1512
2003
|
const before = (await run.store.load()).run;
|
|
1513
2004
|
await persistActiveAgentAttempt(run.store, id, attempt);
|
|
1514
2005
|
const active = (await run.store.load()).run;
|
|
1515
2006
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
1516
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2007
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
1517
2008
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1518
2009
|
};
|
|
1519
2010
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
@@ -1558,7 +2049,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1558
2049
|
catch {
|
|
1559
2050
|
effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : { ...run.model, ...(node.options.thinking ? { thinking: node.options.thinking } : {}) }, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools };
|
|
1560
2051
|
}
|
|
1561
|
-
|
|
2052
|
+
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
2053
|
+
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
|
|
2054
|
+
return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1562
2055
|
});
|
|
1563
2056
|
return { ...current, agents };
|
|
1564
2057
|
});
|
|
@@ -1567,7 +2060,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1567
2060
|
});
|
|
1568
2061
|
const cleanupTerminalRun = async (runId) => {
|
|
1569
2062
|
const run = runs.get(runId);
|
|
1570
|
-
if (!run || !
|
|
2063
|
+
if (!run || !HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
1571
2064
|
return;
|
|
1572
2065
|
await scheduler.cancelRun(runId);
|
|
1573
2066
|
await scheduler.flush();
|
|
@@ -1577,6 +2070,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1577
2070
|
terminalRunStates.set(runId, run.lifecycle.state);
|
|
1578
2071
|
run.checkpointResolvers.clear();
|
|
1579
2072
|
liveActivities.delete(runId);
|
|
2073
|
+
liveEventTimes.delete(runId);
|
|
1580
2074
|
eventPublisher.removeRun(runId);
|
|
1581
2075
|
runs.delete(runId);
|
|
1582
2076
|
};
|
|
@@ -1617,7 +2111,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1617
2111
|
run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
|
|
1618
2112
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1619
2113
|
};
|
|
1620
|
-
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
|
|
2114
|
+
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
|
|
1621
2115
|
const run = runs.get(runId);
|
|
1622
2116
|
if (!run)
|
|
1623
2117
|
return undefined;
|
|
@@ -1625,7 +2119,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1625
2119
|
if (!request)
|
|
1626
2120
|
return undefined;
|
|
1627
2121
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1628
|
-
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
2122
|
+
const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
|
|
1629
2123
|
if (!silent)
|
|
1630
2124
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1631
2125
|
return result;
|
|
@@ -1705,6 +2199,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1705
2199
|
throw mainAgentError(error);
|
|
1706
2200
|
}
|
|
1707
2201
|
},
|
|
2202
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_respond", args, theme)); },
|
|
2203
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_respond", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
1708
2204
|
});
|
|
1709
2205
|
pi.registerTool({
|
|
1710
2206
|
name: "workflow_stop",
|
|
@@ -1720,6 +2216,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1720
2216
|
throw mainAgentError(error);
|
|
1721
2217
|
}
|
|
1722
2218
|
},
|
|
2219
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_stop", args, theme)); },
|
|
2220
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_stop", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
1723
2221
|
});
|
|
1724
2222
|
let catalogRegistered = false;
|
|
1725
2223
|
let sessionStarted = false;
|
|
@@ -1751,37 +2249,100 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1751
2249
|
});
|
|
1752
2250
|
catalogRegistered = true;
|
|
1753
2251
|
};
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2252
|
+
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
2253
|
+
const activeSnapshotTools = (tools, active) => active === "session"
|
|
2254
|
+
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
2255
|
+
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
2256
|
+
const resumeLaunchPrologue = async (input) => {
|
|
2257
|
+
const active = new Set(pi.getActiveTools().filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
2258
|
+
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1758
2259
|
if (missing)
|
|
1759
2260
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1760
2261
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1761
|
-
const
|
|
1762
|
-
const
|
|
1763
|
-
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2262
|
+
const resolution = resolveWorkflowSettings(input.cwd, input.trustedProject, settingsPath);
|
|
2263
|
+
const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
|
|
1764
2264
|
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1765
|
-
const previousAliases =
|
|
2265
|
+
const previousAliases = input.snapshot.modelAliases ?? input.snapshot.settings.modelAliases ?? {};
|
|
2266
|
+
const inventory = modelInventory(input.rootModel, input.modelRegistry);
|
|
2267
|
+
const knownModels = input.modelRegistry ? inventory.knownModels : new Set([...input.snapshot.models, ...inventory.knownModels]);
|
|
2268
|
+
const availableModels = input.modelRegistry ? inventory.availableModels : new Set([...input.snapshot.models, ...inventory.availableModels]);
|
|
2269
|
+
const currentAliases = input.resolvedAliases ?? (await resolveLaunchAliases(registry, staticAliases, { cwd: input.cwd, projectTrusted: input.trustedProject, rootModel: input.rootModel, knownModels, availableModels, signal: input.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
2270
|
+
const blockedAliases = input.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2271
|
+
const blockedAliasTargets = input.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2272
|
+
let script;
|
|
2273
|
+
if (input.withPreflight) {
|
|
2274
|
+
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2275
|
+
script = launchScriptForSnapshot(input.snapshot, registry);
|
|
2276
|
+
preflight(script, { models: availableModels, tools: active, agentTypes: new Set(input.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, input.snapshot.schemas, input.snapshot.metadata, true);
|
|
2277
|
+
}
|
|
2278
|
+
const refreshed = resumedSnapshotSettings(input.snapshot, resolution, currentAliases);
|
|
2279
|
+
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2280
|
+
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
2281
|
+
};
|
|
2282
|
+
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId, captureRole) => async (prompt, options, agentSignal, identity) => {
|
|
2283
|
+
await lifecycle.enter();
|
|
2284
|
+
try {
|
|
2285
|
+
const path = agentIdentityPath(identity);
|
|
2286
|
+
const replayed = await store.replay(path);
|
|
2287
|
+
if (replayed) {
|
|
2288
|
+
return replayed.value;
|
|
2289
|
+
}
|
|
2290
|
+
const worktree = agentWorktree(identity);
|
|
2291
|
+
const agentCwd = worktree.worktreeOwner ? (await persistWorktree(store, metadata, worktree.worktreeOwner)).cwd : cwd;
|
|
2292
|
+
const role = typeof options.role === "string" ? options.role : undefined;
|
|
2293
|
+
const model = typeof options.model === "string" ? options.model : undefined;
|
|
2294
|
+
const thinking = parseThinking(options.thinking);
|
|
2295
|
+
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
2296
|
+
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
2297
|
+
if (role)
|
|
2298
|
+
await captureRole?.(role, resolved.model);
|
|
2299
|
+
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
2300
|
+
const tools = resolved.tools;
|
|
2301
|
+
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
2302
|
+
const spawned = scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd: agentCwd, tools, ...worktree, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), agentOptions: options, agentIdentity: identity });
|
|
2303
|
+
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
2304
|
+
if (agentSignal.aborted)
|
|
2305
|
+
cancel();
|
|
2306
|
+
else
|
|
2307
|
+
agentSignal.addEventListener("abort", cancel, { once: true });
|
|
2308
|
+
const outcome = await spawned.result.finally(() => { agentSignal.removeEventListener("abort", cancel); });
|
|
2309
|
+
if (!outcome.ok)
|
|
2310
|
+
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
2311
|
+
await store.complete(path, outcome.value);
|
|
2312
|
+
return outcome.value;
|
|
2313
|
+
}
|
|
2314
|
+
finally {
|
|
2315
|
+
await lifecycle.leave();
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
const refreshPausedRunAliases = async (run, context) => {
|
|
2319
|
+
const loaded = await run.store.load();
|
|
2320
|
+
const trustedProject = context?.projectTrusted ?? run.projectTrusted();
|
|
1766
2321
|
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1767
|
-
const
|
|
1768
|
-
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1769
|
-
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1770
|
-
const currentAliases = (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: run.abortController.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
1771
|
-
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1772
|
-
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1773
|
-
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1774
|
-
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2322
|
+
const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: run.abortController.signal, withPreflight: false });
|
|
1775
2323
|
await run.store.saveSnapshot(snapshot);
|
|
1776
2324
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1777
|
-
run.executor =
|
|
2325
|
+
run.executor = createAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: activeSnapshotTools(snapshot.tools, "session"), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
|
|
1778
2326
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
1779
2327
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1780
2328
|
if (drift.length)
|
|
1781
2329
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1782
2330
|
};
|
|
1783
|
-
const
|
|
2331
|
+
const recoveryUi = (context) => {
|
|
2332
|
+
const host = object(context) ? context : undefined;
|
|
2333
|
+
const ui = host && object(host.ui) ? host.ui : {};
|
|
2334
|
+
return { hasUI: host?.hasUI === true, ui };
|
|
2335
|
+
};
|
|
2336
|
+
const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
|
|
1784
2337
|
const loaded = await run.store.load();
|
|
2338
|
+
const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
|
|
2339
|
+
if (loaded.run.activeShells !== undefined) {
|
|
2340
|
+
await persistRunState(run.store, run.metadata, (current) => {
|
|
2341
|
+
const next = { ...current };
|
|
2342
|
+
delete next.activeShells;
|
|
2343
|
+
return next;
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
1785
2346
|
await run.store.validateRetrySource();
|
|
1786
2347
|
await run.store.validateBorrowedWorktrees();
|
|
1787
2348
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
|
|
@@ -1793,19 +2354,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1793
2354
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
1794
2355
|
if (missingRole)
|
|
1795
2356
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
|
|
1796
|
-
const active = new Set(pi.getActiveTools().filter((tool) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(tool)));
|
|
1797
|
-
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1798
|
-
if (missing)
|
|
1799
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1800
|
-
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1801
|
-
const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
|
|
1802
|
-
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1803
|
-
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1804
|
-
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1805
2357
|
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1806
|
-
const inventory = modelInventory(rootModel, context?.modelRegistry);
|
|
1807
|
-
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1808
|
-
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1809
2358
|
const controller = new AbortController();
|
|
1810
2359
|
if (context?.signal?.aborted)
|
|
1811
2360
|
controller.abort();
|
|
@@ -1813,17 +2362,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1813
2362
|
context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
|
|
1814
2363
|
}
|
|
1815
2364
|
run.abortController = controller;
|
|
1816
|
-
const currentAliases
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
1822
|
-
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1823
|
-
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1824
|
-
await run.store.saveSnapshot(snapshot);
|
|
2365
|
+
const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
|
|
2366
|
+
if (!script)
|
|
2367
|
+
throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
2368
|
+
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
2369
|
+
await run.store.saveSnapshot(persistedSnapshot);
|
|
1825
2370
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1826
|
-
run.executor =
|
|
2371
|
+
run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
|
|
1827
2372
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1828
2373
|
if (drift.length)
|
|
1829
2374
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
@@ -1835,9 +2380,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1835
2380
|
}
|
|
1836
2381
|
catch (error) {
|
|
1837
2382
|
const typed = asWorkflowError(error);
|
|
1838
|
-
if (!
|
|
2383
|
+
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
|
|
1839
2384
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
1840
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current
|
|
2385
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed));
|
|
1841
2386
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
1842
2387
|
run.update?.(workflowToolUpdate(persisted));
|
|
1843
2388
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
@@ -1848,37 +2393,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1848
2393
|
}
|
|
1849
2394
|
await scheduler.cancelRun(run.store.runId);
|
|
1850
2395
|
await run.lifecycle.resume();
|
|
1851
|
-
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: async (
|
|
1852
|
-
await run.lifecycle.enter();
|
|
1853
|
-
try {
|
|
1854
|
-
const path = agentIdentityPath(identity);
|
|
1855
|
-
const replayed = await run.store.replay(path);
|
|
1856
|
-
if (replayed) {
|
|
1857
|
-
return replayed.value;
|
|
1858
|
-
}
|
|
1859
|
-
const worktree = agentWorktree(identity);
|
|
1860
|
-
const cwd = worktree.worktreeOwner ? (await persistWorktree(run.store, run.metadata, worktree.worktreeOwner)).cwd : run.store.cwd;
|
|
1861
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
1862
|
-
const model = typeof options.model === "string" ? options.model : undefined;
|
|
1863
|
-
const thinking = parseThinking(options.thinking);
|
|
1864
|
-
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1865
|
-
const resolved = run.executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
1866
|
-
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
1867
|
-
const tools = resolved.tools;
|
|
1868
|
-
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
1869
|
-
const spawned = scheduler.spawn(run.store.runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd, tools, ...worktree, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), agentOptions: options, agentIdentity: identity });
|
|
1870
|
-
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
1871
|
-
signal.addEventListener("abort", cancel, { once: true });
|
|
1872
|
-
const outcome = await spawned.result.finally(() => { signal.removeEventListener("abort", cancel); });
|
|
1873
|
-
if (!outcome.ok)
|
|
1874
|
-
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
1875
|
-
await run.store.complete(path, outcome.value);
|
|
1876
|
-
return outcome.value;
|
|
1877
|
-
}
|
|
1878
|
-
finally {
|
|
1879
|
-
await run.lifecycle.leave();
|
|
1880
|
-
}
|
|
1881
|
-
}, worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
2396
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
1882
2397
|
run.execution = execution;
|
|
1883
2398
|
const completion = execution.result.then(async (value) => {
|
|
1884
2399
|
await scheduler.flush();
|
|
@@ -1893,19 +2408,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1893
2408
|
const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
|
|
1894
2409
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
1895
2410
|
await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
1896
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot()
|
|
2411
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
|
|
1897
2412
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
1898
2413
|
if (state === "failed")
|
|
1899
2414
|
retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
|
|
1900
2415
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
1901
2416
|
run.update?.(workflowToolUpdate(persisted));
|
|
1902
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
1903
|
-
await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted)
|
|
2417
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) {
|
|
2418
|
+
const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
|
|
2419
|
+
Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
|
|
2420
|
+
}
|
|
2421
|
+
throw typed;
|
|
1904
2422
|
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
1905
2423
|
run.completion = completion;
|
|
1906
|
-
|
|
2424
|
+
if (!foreground || !waitForCompletion) {
|
|
2425
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2426
|
+
deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
|
|
2427
|
+
}, (error) => {
|
|
2428
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2429
|
+
if (diagnostic)
|
|
2430
|
+
deliverFailure(pi, diagnostic);
|
|
2431
|
+
else
|
|
2432
|
+
deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
|
|
2433
|
+
});
|
|
2434
|
+
return undefined;
|
|
2435
|
+
}
|
|
2436
|
+
return completion;
|
|
1907
2437
|
};
|
|
1908
|
-
const applyBudgetDecision = async (request, approved, context, signal) => {
|
|
2438
|
+
const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
|
|
1909
2439
|
const run = runs.get(request.runId);
|
|
1910
2440
|
if (!run)
|
|
1911
2441
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
@@ -1919,16 +2449,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1919
2449
|
next.budget = nextBudget;
|
|
1920
2450
|
else
|
|
1921
2451
|
delete next.budget; return next; });
|
|
1922
|
-
|
|
2452
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2453
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
|
|
2454
|
+
if (completed)
|
|
2455
|
+
return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
|
|
1923
2456
|
return { state: "running", approved: true };
|
|
1924
2457
|
};
|
|
1925
|
-
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
2458
|
+
const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true) => {
|
|
1926
2459
|
const run = runs.get(runId);
|
|
1927
|
-
if (!run)
|
|
1928
|
-
|
|
2460
|
+
if (!run) {
|
|
2461
|
+
const host = object(context) ? context : {};
|
|
2462
|
+
const cwd = typeof host.cwd === "string" ? host.cwd : undefined;
|
|
2463
|
+
const sessionManager = object(host.sessionManager) ? host.sessionManager : undefined;
|
|
2464
|
+
const sessionId = typeof sessionManager?.getSessionId === "function" ? String(Reflect.apply(sessionManager.getSessionId, sessionManager, [])) : undefined;
|
|
2465
|
+
if (cwd && sessionId) {
|
|
2466
|
+
try {
|
|
2467
|
+
const state = (await new RunStore(cwd, sessionId, runId, home).load()).run.state;
|
|
2468
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", state));
|
|
2469
|
+
}
|
|
2470
|
+
catch (error) {
|
|
2471
|
+
if (error instanceof WorkflowError)
|
|
2472
|
+
throw error;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session`);
|
|
2476
|
+
}
|
|
1929
2477
|
const loaded = await run.store.load();
|
|
1930
2478
|
if (loaded.run.state !== "budget_exhausted")
|
|
1931
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "
|
|
2479
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", loaded.run.state));
|
|
1932
2480
|
const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1933
2481
|
const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
|
|
1934
2482
|
const nextBudget = mergeBudget(currentBudget, patch);
|
|
@@ -1953,11 +2501,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1953
2501
|
else
|
|
1954
2502
|
delete next.budget; return next; });
|
|
1955
2503
|
}
|
|
1956
|
-
|
|
2504
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2505
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
|
|
2506
|
+
if (completed)
|
|
2507
|
+
return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run };
|
|
1957
2508
|
return { state: "running" };
|
|
1958
2509
|
};
|
|
1959
2510
|
const retryReservations = new Set();
|
|
1960
|
-
const retryWorkflowRun = async (runId, context, signal) => {
|
|
2511
|
+
const retryWorkflowRun = async (runId, context, signal, modeOverride) => {
|
|
1961
2512
|
if (typeof runId !== "string" || !runId.trim())
|
|
1962
2513
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
1963
2514
|
const host = object(context) ? context : {};
|
|
@@ -1973,10 +2524,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1973
2524
|
loaded = await sourceStore.load();
|
|
1974
2525
|
}
|
|
1975
2526
|
catch (error) {
|
|
1976
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `
|
|
2527
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session: ${errorText(error)}`);
|
|
1977
2528
|
}
|
|
1978
2529
|
if (loaded.run.state !== "failed")
|
|
1979
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE",
|
|
2530
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("retry", loaded.run.state));
|
|
1980
2531
|
if (loaded.run.retry && (typeof loaded.run.retry.sourceRunId !== "string" || !loaded.run.retry.sourceRunId || typeof loaded.run.retry.lineageRootRunId !== "string" || !loaded.run.retry.lineageRootRunId || !Array.isArray(loaded.run.retry.completedPaths) || loaded.run.retry.completedPaths.some((path) => typeof path !== "string") || !Array.isArray(loaded.run.retry.incompletePaths) || loaded.run.retry.incompletePaths.some((path) => typeof path !== "string") || !Array.isArray(loaded.run.retry.namedWorktrees) || loaded.run.retry.namedWorktrees.some((name) => typeof name !== "string")))
|
|
1981
2532
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "The source retry provenance is incomplete");
|
|
1982
2533
|
const lineageRootRunId = loaded.run.retry?.lineageRootRunId ?? loaded.run.id;
|
|
@@ -2012,28 +2563,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2012
2563
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
2013
2564
|
if (missingRole)
|
|
2014
2565
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
|
|
2015
|
-
const active = new Set(pi.getActiveTools().filter((tool) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(tool)));
|
|
2016
|
-
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2017
|
-
if (missing)
|
|
2018
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2019
|
-
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2020
|
-
const resolution = resolveWorkflowSettings(cwd, trustedProject, settingsPath);
|
|
2021
|
-
const currentPolicy = resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
2022
|
-
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
2023
|
-
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
2024
2566
|
const modelRegistry = contextHostCapabilities(context).modelRegistry;
|
|
2025
2567
|
const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
|
|
2026
2568
|
const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
2027
|
-
const
|
|
2028
|
-
const knownModels = modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
2029
|
-
const availableModels = modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
2030
|
-
const resolvedAliases = await resolveLaunchAliases(registry, staticAliases, { cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: signal ?? new AbortController().signal }, availableModels, knownModels, settingsPath);
|
|
2031
|
-
const currentAliases = resolvedAliases.aliases;
|
|
2032
|
-
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2033
|
-
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2034
|
-
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2035
|
-
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
2036
|
-
preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
2569
|
+
const { active, settingsPath, currentPolicy, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot: childBaseSnapshot } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd, trustedProject, rootModel, ...(modelRegistry ? { modelRegistry } : {}), signal: signal ?? new AbortController().signal, withPreflight: true });
|
|
2037
2570
|
await sourceStore.validateNamedWorktrees();
|
|
2038
2571
|
for (const name of loaded.run.retry?.namedWorktrees ?? [])
|
|
2039
2572
|
await sourceStore.resolveNamedWorktree(name);
|
|
@@ -2043,8 +2576,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2043
2576
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
2044
2577
|
const childRunId = randomUUID();
|
|
2045
2578
|
const childStore = new RunStore(cwd, sessionId, childRunId, home);
|
|
2046
|
-
const
|
|
2047
|
-
const childSnapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2579
|
+
const childSnapshot = childBaseSnapshot;
|
|
2048
2580
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
2049
2581
|
const childInitialBudget = childBudget.snapshot();
|
|
2050
2582
|
const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
@@ -2055,16 +2587,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2055
2587
|
const abortController = new AbortController();
|
|
2056
2588
|
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
2057
2589
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2058
|
-
const childRun = { executor:
|
|
2590
|
+
const childRun = { executor: createAgentExecutor({ cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentResourcePolicy: frozenResourcePolicy(currentPolicy) }), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
|
|
2059
2591
|
runs.set(childRunId, childRun);
|
|
2060
2592
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2061
2593
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
2062
|
-
|
|
2594
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2595
|
+
const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
2063
2596
|
const completion = runs.get(childRunId)?.completion;
|
|
2064
2597
|
if (completion) {
|
|
2065
2598
|
childStarted = true;
|
|
2066
2599
|
void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
|
|
2067
2600
|
}
|
|
2601
|
+
else if (completed) {
|
|
2602
|
+
childStarted = true;
|
|
2603
|
+
retryReservations.delete(lineageRootRunId);
|
|
2604
|
+
}
|
|
2605
|
+
if (completed)
|
|
2606
|
+
return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
|
|
2068
2607
|
return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
|
|
2069
2608
|
}
|
|
2070
2609
|
finally {
|
|
@@ -2079,28 +2618,32 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2079
2618
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
2080
2619
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2081
2620
|
try {
|
|
2082
|
-
const result = await retryWorkflowRun(params.runId, ctx, signal);
|
|
2621
|
+
const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground);
|
|
2083
2622
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2084
2623
|
}
|
|
2085
2624
|
catch (error) {
|
|
2086
2625
|
throw mainAgentError(error);
|
|
2087
2626
|
}
|
|
2088
2627
|
},
|
|
2628
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_retry", args, theme)); },
|
|
2629
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_retry", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
2089
2630
|
});
|
|
2090
2631
|
pi.registerTool({
|
|
2091
2632
|
name: "workflow_resume",
|
|
2092
2633
|
label: "Workflow Resume",
|
|
2093
2634
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
2094
|
-
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
2635
|
+
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
|
|
2095
2636
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2096
2637
|
try {
|
|
2097
|
-
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
|
|
2638
|
+
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground);
|
|
2098
2639
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2099
2640
|
}
|
|
2100
2641
|
catch (error) {
|
|
2101
2642
|
throw mainAgentError(error);
|
|
2102
2643
|
}
|
|
2103
2644
|
},
|
|
2645
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_resume", args, theme)); },
|
|
2646
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_resume", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
2104
2647
|
});
|
|
2105
2648
|
pi.on("session_start", async (_event, ctx) => {
|
|
2106
2649
|
if (sessionStarted)
|
|
@@ -2129,11 +2672,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2129
2672
|
}
|
|
2130
2673
|
if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
|
|
2131
2674
|
const previousState = loaded.run.state;
|
|
2132
|
-
await store.updateState((current) =>
|
|
2675
|
+
await store.updateState((current) => {
|
|
2676
|
+
if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state))
|
|
2677
|
+
return current;
|
|
2678
|
+
const next = { ...current, state: "interrupted" };
|
|
2679
|
+
delete next.activeShells;
|
|
2680
|
+
return next;
|
|
2681
|
+
});
|
|
2133
2682
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2134
2683
|
await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
|
|
2135
2684
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2136
2685
|
}
|
|
2686
|
+
else if (loaded.run.activeShells !== undefined) {
|
|
2687
|
+
await store.updateState((current) => {
|
|
2688
|
+
if (["completed", "failed", "stopped"].includes(current.state))
|
|
2689
|
+
return current;
|
|
2690
|
+
const next = { ...current };
|
|
2691
|
+
delete next.activeShells;
|
|
2692
|
+
return next;
|
|
2693
|
+
});
|
|
2694
|
+
loaded = { ...loaded, run: (await store.load()).run };
|
|
2695
|
+
}
|
|
2137
2696
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
|
|
2138
2697
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
2139
2698
|
eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
|
|
@@ -2143,7 +2702,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2143
2702
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
2144
2703
|
const abortController = new AbortController();
|
|
2145
2704
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
2146
|
-
runs.set(runId, { executor:
|
|
2705
|
+
runs.set(runId, { executor: createAgentExecutor({ cwd: ctx.cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, "session"), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentResourcePolicy: frozenResourcePolicy(snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir))) }), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
|
|
2147
2706
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
2148
2707
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
2149
2708
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -2161,7 +2720,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2161
2720
|
const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
|
|
2162
2721
|
for (const run of toResume) {
|
|
2163
2722
|
try {
|
|
2164
|
-
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
|
|
2723
|
+
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2165
2724
|
ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
|
|
2166
2725
|
}
|
|
2167
2726
|
catch (err) {
|
|
@@ -2205,7 +2764,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2205
2764
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
2206
2765
|
const knownModels = inventory.knownModels;
|
|
2207
2766
|
const availableModels = inventory.availableModels;
|
|
2208
|
-
const rootTools = pi.getActiveTools().filter((name) => !
|
|
2767
|
+
const rootTools = pi.getActiveTools().filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
2209
2768
|
const trustedProject = projectTrusted(ctx);
|
|
2210
2769
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
2211
2770
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -2233,54 +2792,38 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2233
2792
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2234
2793
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
2235
2794
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
2236
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2795
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2796
|
+
let persistedSnapshot = snapshot;
|
|
2797
|
+
const captureFunctionRole = functionName ? async (role, model) => {
|
|
2798
|
+
const definition = agentDefinitions[role];
|
|
2799
|
+
if (!definition)
|
|
2800
|
+
return;
|
|
2801
|
+
const modelName = `${model.provider}/${model.model}`;
|
|
2802
|
+
const hasProjectRole = projectAgentDefinitions[role] !== undefined;
|
|
2803
|
+
if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName))
|
|
2804
|
+
return;
|
|
2805
|
+
const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
|
|
2806
|
+
const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
|
|
2807
|
+
const models = [...new Set([...persistedSnapshot.models, modelName])];
|
|
2808
|
+
persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
|
|
2809
|
+
await store.saveSnapshot(persistedSnapshot);
|
|
2810
|
+
} : undefined;
|
|
2237
2811
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2238
2812
|
const initialBudget = budgetRuntime.snapshot();
|
|
2239
|
-
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2813
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2814
|
+
if (params.foreground)
|
|
2815
|
+
foregroundDeliveries.set(toolCallId, { store, inline: false });
|
|
2240
2816
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2241
2817
|
const background = !params.foreground;
|
|
2242
2818
|
const providerPause = async () => { if (background)
|
|
2243
2819
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2244
2820
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
2245
|
-
const executor =
|
|
2821
|
+
const executor = createAgentExecutor({ cwd: ctx.cwd, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases, settingsPath, agentDefinitions, runStore: store, providerPause, agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy), runContext });
|
|
2246
2822
|
runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
|
|
2247
2823
|
if (params.foreground && onUpdate)
|
|
2248
2824
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2249
2825
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2250
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: async (
|
|
2251
|
-
await lifecycle.enter();
|
|
2252
|
-
try {
|
|
2253
|
-
const path = agentIdentityPath(identity);
|
|
2254
|
-
const replayed = await store.replay(path);
|
|
2255
|
-
if (replayed) {
|
|
2256
|
-
return replayed.value;
|
|
2257
|
-
}
|
|
2258
|
-
const worktree = agentWorktree(identity);
|
|
2259
|
-
const cwd = worktree.worktreeOwner ? (await persistWorktree(store, checked.metadata, worktree.worktreeOwner)).cwd : ctx.cwd;
|
|
2260
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
2261
|
-
const model = typeof options.model === "string" ? options.model : undefined;
|
|
2262
|
-
const thinking = parseThinking(options.thinking);
|
|
2263
|
-
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
2264
|
-
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: checked.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
2265
|
-
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
2266
|
-
const tools = resolved.tools;
|
|
2267
|
-
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
2268
|
-
const spawned = scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd, tools, ...worktree, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), agentOptions: options, agentIdentity: identity });
|
|
2269
|
-
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
2270
|
-
if (agentSignal.aborted)
|
|
2271
|
-
cancel();
|
|
2272
|
-
else
|
|
2273
|
-
agentSignal.addEventListener("abort", cancel, { once: true });
|
|
2274
|
-
const outcome = await spawned.result.finally(() => { agentSignal.removeEventListener("abort", cancel); });
|
|
2275
|
-
if (!outcome.ok)
|
|
2276
|
-
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
2277
|
-
await store.complete(path, outcome.value);
|
|
2278
|
-
return outcome.value;
|
|
2279
|
-
}
|
|
2280
|
-
finally {
|
|
2281
|
-
await lifecycle.leave();
|
|
2282
|
-
}
|
|
2283
|
-
}, worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2826
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, captureFunctionRole), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2284
2827
|
runs.get(runId).execution = execution;
|
|
2285
2828
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2286
2829
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2296,7 +2839,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2296
2839
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2297
2840
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
|
|
2298
2841
|
await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2299
|
-
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot()
|
|
2842
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
|
|
2300
2843
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2301
2844
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
2302
2845
|
const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
@@ -2307,18 +2850,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2307
2850
|
});
|
|
2308
2851
|
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
2309
2852
|
runs.get(runId).completion = completion;
|
|
2853
|
+
const deliverFailureContent = (error) => {
|
|
2854
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2855
|
+
return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
|
|
2856
|
+
};
|
|
2857
|
+
const queueForegroundDelivery = async (content) => {
|
|
2858
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
2859
|
+
if (!delivery)
|
|
2860
|
+
return;
|
|
2861
|
+
await store.updateState((current) => {
|
|
2862
|
+
if (!current.delivery || current.delivery.state === "delivered")
|
|
2863
|
+
return current;
|
|
2864
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
|
|
2865
|
+
});
|
|
2866
|
+
if (delivery.inline)
|
|
2867
|
+
return;
|
|
2868
|
+
scheduleForegroundDelivery(toolCallId, async () => {
|
|
2869
|
+
if (delivery.inline)
|
|
2870
|
+
return;
|
|
2871
|
+
pendingFailureDiagnostics.delete(toolCallId);
|
|
2872
|
+
await deliverTerminal(store, content);
|
|
2873
|
+
});
|
|
2874
|
+
};
|
|
2310
2875
|
if (background) {
|
|
2311
2876
|
void completion.then(async ({ value, resultPath }) => {
|
|
2312
|
-
|
|
2313
|
-
}, (error) => {
|
|
2314
|
-
|
|
2315
|
-
if (diagnostic)
|
|
2316
|
-
deliverFailure(pi, diagnostic);
|
|
2317
|
-
else
|
|
2318
|
-
deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
2877
|
+
await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2878
|
+
}, async (error) => {
|
|
2879
|
+
await deliverTerminal(store, deliverFailureContent(error));
|
|
2319
2880
|
});
|
|
2320
2881
|
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2321
2882
|
}
|
|
2883
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2884
|
+
await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2885
|
+
}, async (error) => {
|
|
2886
|
+
await queueForegroundDelivery(deliverFailureContent(error));
|
|
2887
|
+
});
|
|
2322
2888
|
const { value } = await completion;
|
|
2323
2889
|
const run = (await store.load()).run;
|
|
2324
2890
|
return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
@@ -2365,7 +2931,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2365
2931
|
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
2366
2932
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
2367
2933
|
try {
|
|
2368
|
-
|
|
2934
|
+
const loaded = await store.load();
|
|
2935
|
+
return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } };
|
|
2369
2936
|
}
|
|
2370
2937
|
catch {
|
|
2371
2938
|
if (!await store.isComplete())
|
|
@@ -2393,12 +2960,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2393
2960
|
return keepContext ? "dashboard" : "done";
|
|
2394
2961
|
}
|
|
2395
2962
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2396
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2963
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
2397
2964
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2398
2965
|
return keepContext ? "dashboard" : "done";
|
|
2399
2966
|
}
|
|
2400
2967
|
if (action === "delete" && stored) {
|
|
2401
|
-
if (!
|
|
2968
|
+
if (!HARD_TERMINAL_RUN_STATES.has(stored.loaded.run.state)) {
|
|
2402
2969
|
ctx.ui.notify("Stop the workflow before deleting it.", "warning");
|
|
2403
2970
|
return keepContext ? "dashboard" : "done";
|
|
2404
2971
|
}
|
|
@@ -2418,12 +2985,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2418
2985
|
if (action === "resume" && run) {
|
|
2419
2986
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2420
2987
|
const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
|
|
2421
|
-
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2422
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
2988
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
2989
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2423
2990
|
}
|
|
2424
2991
|
else {
|
|
2425
2992
|
if (run.lifecycle.state === "interrupted")
|
|
2426
|
-
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2993
|
+
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2427
2994
|
else {
|
|
2428
2995
|
if (run.lifecycle.state === "paused")
|
|
2429
2996
|
await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
@@ -2437,8 +3004,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2437
3004
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
2438
3005
|
if (input === undefined)
|
|
2439
3006
|
return keepContext ? "dashboard" : "done";
|
|
2440
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2441
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
3007
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
3008
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2442
3009
|
return keepContext ? "dashboard" : "done";
|
|
2443
3010
|
}
|
|
2444
3011
|
if (action === "stop" && run) {
|
|
@@ -2591,7 +3158,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2591
3158
|
}
|
|
2592
3159
|
const sorted = navigatorAttentionSort(stores);
|
|
2593
3160
|
const labels = navigatorRunLabels(sorted);
|
|
2594
|
-
const terminalStates =
|
|
3161
|
+
const terminalStates = HARD_TERMINAL_RUN_STATES;
|
|
2595
3162
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
2596
3163
|
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
2597
3164
|
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
@@ -2659,18 +3226,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2659
3226
|
};
|
|
2660
3227
|
const loadDashboard = async () => {
|
|
2661
3228
|
const loaded = await store.load();
|
|
3229
|
+
const liveRun = withLiveActivities(loaded.run);
|
|
2662
3230
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2663
3231
|
const worktrees = await store.worktrees();
|
|
3232
|
+
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
3233
|
+
const agentResults = new Map();
|
|
3234
|
+
for (const agent of liveRun.agents) {
|
|
3235
|
+
if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
|
|
3236
|
+
continue;
|
|
3237
|
+
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
3238
|
+
if (operation)
|
|
3239
|
+
agentResults.set(agent.id, operation.value);
|
|
3240
|
+
}
|
|
2664
3241
|
const actions = new Map();
|
|
2665
3242
|
const copies = new Map();
|
|
2666
3243
|
const reviews = new Map();
|
|
2667
3244
|
const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
|
|
2668
3245
|
const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
2669
|
-
if (
|
|
3246
|
+
if (liveRun.state === "running")
|
|
2670
3247
|
add("Pause", "pause");
|
|
2671
|
-
if (["paused", "interrupted"].includes(
|
|
3248
|
+
if (["paused", "interrupted"].includes(liveRun.state))
|
|
2672
3249
|
add("Resume", "resume");
|
|
2673
|
-
if (
|
|
3250
|
+
if (liveRun.state === "budget_exhausted") {
|
|
2674
3251
|
actions.set("Resume unchanged", `resume ${store.runId}`);
|
|
2675
3252
|
actions.set("Adjust budget", `adjust ${store.runId}`);
|
|
2676
3253
|
}
|
|
@@ -2679,7 +3256,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2679
3256
|
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
2680
3257
|
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
2681
3258
|
}
|
|
2682
|
-
if (!terminalStates.has(
|
|
3259
|
+
if (!terminalStates.has(liveRun.state))
|
|
2683
3260
|
add("Stop", "stop");
|
|
2684
3261
|
for (const cp of checkpoints) {
|
|
2685
3262
|
if (ctx.mode === "tui") {
|
|
@@ -2695,40 +3272,64 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2695
3272
|
if (ctx.mode !== "tui")
|
|
2696
3273
|
actions.set("Refresh", "refresh");
|
|
2697
3274
|
else
|
|
2698
|
-
actions.set("
|
|
2699
|
-
if (
|
|
3275
|
+
actions.set("Open script in editor", "open-script");
|
|
3276
|
+
if (ctx.mode !== "tui" && liveRun.agents.length)
|
|
2700
3277
|
actions.set("Agents...", "agents");
|
|
2701
|
-
if (terminalStates.has(
|
|
3278
|
+
if (terminalStates.has(liveRun.state))
|
|
2702
3279
|
add("Delete", "delete");
|
|
2703
3280
|
if (ctx.mode === "tui") {
|
|
2704
3281
|
addCopy("Copy run path", store.directory, "run path");
|
|
2705
3282
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2706
3283
|
}
|
|
2707
|
-
return { dashboard: formatWorkflowPhaseDashboard(
|
|
3284
|
+
return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
|
|
3285
|
+
};
|
|
3286
|
+
const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
3287
|
+
const agentActionLabels = (dashboard, agent) => {
|
|
3288
|
+
const worktree = agentWorktreeFor(dashboard, agent);
|
|
3289
|
+
return [
|
|
3290
|
+
...(agent.attemptDetails?.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3291
|
+
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3292
|
+
...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
|
|
3293
|
+
"Copy agent ID",
|
|
3294
|
+
"Back",
|
|
3295
|
+
];
|
|
3296
|
+
};
|
|
3297
|
+
const forkAgentSession = async (dashboard, agent) => {
|
|
3298
|
+
const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3299
|
+
const worktree = agentWorktreeFor(dashboard, agent);
|
|
3300
|
+
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3301
|
+
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
3302
|
+
const index = choice ? choices.indexOf(choice) : -1;
|
|
3303
|
+
const attempt = index >= 0 ? attempts[index] : undefined;
|
|
3304
|
+
if (!attempt)
|
|
3305
|
+
return;
|
|
3306
|
+
const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
3307
|
+
if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?"))
|
|
3308
|
+
return;
|
|
3309
|
+
try {
|
|
3310
|
+
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
3311
|
+
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
3312
|
+
}
|
|
3313
|
+
catch (error) {
|
|
3314
|
+
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3315
|
+
}
|
|
2708
3316
|
};
|
|
2709
|
-
const selectAgent = async (dashboard) => {
|
|
3317
|
+
const selectAgent = async (dashboard, requestedAgentId) => {
|
|
2710
3318
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
2711
3319
|
const title = (agent) => agentBreadcrumb(agent, byId, true);
|
|
2712
3320
|
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3321
|
+
let selected;
|
|
3322
|
+
if (requestedAgentId)
|
|
3323
|
+
selected = dashboard.agents.find((agent) => agent.id === requestedAgentId);
|
|
3324
|
+
else {
|
|
3325
|
+
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
3326
|
+
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
3327
|
+
selected = selectedIndex >= 0 ? dashboard.agents[selectedIndex] : undefined;
|
|
3328
|
+
}
|
|
2716
3329
|
if (!selected)
|
|
2717
3330
|
return;
|
|
2718
|
-
const
|
|
2719
|
-
const
|
|
2720
|
-
const actions = [
|
|
2721
|
-
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
2722
|
-
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
2723
|
-
"Copy agent ID",
|
|
2724
|
-
"Back",
|
|
2725
|
-
];
|
|
2726
|
-
const chooseAttempt = async () => {
|
|
2727
|
-
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
2728
|
-
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
2729
|
-
const index = choice ? choices.indexOf(choice) : -1;
|
|
2730
|
-
return index >= 0 ? attempts[index] : undefined;
|
|
2731
|
-
};
|
|
3331
|
+
const worktree = agentWorktreeFor(dashboard, selected);
|
|
3332
|
+
const actions = agentActionLabels(dashboard, selected);
|
|
2732
3333
|
for (;;) {
|
|
2733
3334
|
const action = await ctx.ui.select(title(selected), actions);
|
|
2734
3335
|
if (!action || action === "Back")
|
|
@@ -2745,63 +3346,83 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2745
3346
|
await copyArtifact(worktree.path, "worktree path");
|
|
2746
3347
|
continue;
|
|
2747
3348
|
}
|
|
2748
|
-
if (action === "Fork as Pi session in pane")
|
|
2749
|
-
|
|
2750
|
-
if (!attempt)
|
|
2751
|
-
continue;
|
|
2752
|
-
const running = !SETTLED_AGENT_STATES.has(selected.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
2753
|
-
if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?"))
|
|
2754
|
-
continue;
|
|
2755
|
-
try {
|
|
2756
|
-
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
2757
|
-
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
2758
|
-
}
|
|
2759
|
-
catch (error) {
|
|
2760
|
-
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
3349
|
+
if (action === "Fork as Pi session in pane")
|
|
3350
|
+
await forkAgentSession(dashboard, selected);
|
|
2763
3351
|
}
|
|
2764
3352
|
};
|
|
2765
3353
|
for (;;) {
|
|
2766
3354
|
let view = await loadDashboard();
|
|
2767
3355
|
const actionChoice = ctx.mode === "tui"
|
|
2768
3356
|
? await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
2769
|
-
let options = [...view.actions.keys(), "Close"];
|
|
2770
|
-
let selectedIndex = 0;
|
|
2771
3357
|
let dashboardOffset = 0;
|
|
2772
3358
|
let refreshing = false;
|
|
2773
3359
|
let disposed = false;
|
|
3360
|
+
let detailsMode = false;
|
|
3361
|
+
let actionMode = false;
|
|
3362
|
+
let actionIndex = 0;
|
|
2774
3363
|
let stopRequested = false;
|
|
2775
3364
|
let stopStatus;
|
|
3365
|
+
let selectionNeedsScroll = true;
|
|
3366
|
+
let renderedWidth = 80;
|
|
3367
|
+
let refreshGeneration = 0;
|
|
2776
3368
|
const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
|
|
2777
|
-
let
|
|
2778
|
-
let
|
|
2779
|
-
|
|
2780
|
-
const
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
3369
|
+
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3370
|
+
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
3371
|
+
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
3372
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
3373
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3374
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3375
|
+
const selectedAgentRecord = () => {
|
|
3376
|
+
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3377
|
+
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
2784
3378
|
};
|
|
2785
|
-
const
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
const separatorRows = rows >= 8 ? 1 : 0;
|
|
2789
|
-
const available = Math.max(1, rows - hintRows - separatorRows);
|
|
2790
|
-
const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
|
|
2791
|
-
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
3379
|
+
const actionOptions = () => {
|
|
3380
|
+
const agent = selectedAgentRecord();
|
|
3381
|
+
return agent ? agentActionLabels(view, agent) : [...view.actions.keys(), "Back"];
|
|
2792
3382
|
};
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3383
|
+
let editorRunning = false;
|
|
3384
|
+
const openArtifact = async (artifact, label) => {
|
|
3385
|
+
if (editorRunning)
|
|
3386
|
+
return;
|
|
3387
|
+
editorRunning = true;
|
|
3388
|
+
try {
|
|
3389
|
+
const command = SettingsManager.create(view.cwd, extensionAgentDir, { projectTrusted: projectTrusted(ctx) }).getExternalEditorCommand();
|
|
3390
|
+
if (!command) {
|
|
3391
|
+
ctx.ui.notify(`Cannot open ${label}: no external editor is configured.`, "warning");
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const exitCode = await openWorkflowArtifact(tui, command, await artifact);
|
|
3395
|
+
if (exitCode !== 0) {
|
|
3396
|
+
const detail = exitCode === null ? "could not be started" : `exited with code ${String(exitCode)}`;
|
|
3397
|
+
ctx.ui.notify(`Cannot open ${label}: external editor ${detail}.`, "warning");
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
catch (error) {
|
|
3401
|
+
ctx.ui.notify(`Cannot open ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3402
|
+
}
|
|
3403
|
+
finally {
|
|
3404
|
+
editorRunning = false;
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
const updateDashboard = async () => {
|
|
3408
|
+
const generation = ++refreshGeneration;
|
|
3409
|
+
const hadExpandableNodes = tree.nodes.some((node) => node.children.length > 0);
|
|
2796
3410
|
const next = await loadDashboard();
|
|
2797
|
-
if (disposed)
|
|
3411
|
+
if (disposed || generation !== refreshGeneration)
|
|
2798
3412
|
return;
|
|
3413
|
+
const previousNodeId = selectedNodeId;
|
|
3414
|
+
const previousExpanded = expandedNodeIds;
|
|
3415
|
+
const selectedAction = actionMode ? actionOptions()[actionIndex] : undefined;
|
|
2799
3416
|
view = next;
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3417
|
+
tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3418
|
+
selectedNodeId = preserveWorkflowPhaseTreeSelection(tree, { nodeId: previousNodeId }).nodeId;
|
|
3419
|
+
expandedNodeIds = new Set([...previousExpanded].filter((id) => tree.byId.has(id)));
|
|
3420
|
+
if (!hadExpandableNodes && !expandedNodeIds.size && tree.nodes.some((node) => node.children.length > 0))
|
|
3421
|
+
expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
3422
|
+
const nextActions = actionOptions();
|
|
3423
|
+
const preservedActionIndex = selectedAction ? nextActions.indexOf(selectedAction) : -1;
|
|
3424
|
+
actionIndex = preservedActionIndex >= 0 ? preservedActionIndex : selectedAction ? nextActions.length - 1 : Math.min(actionIndex, Math.max(0, nextActions.length - 1));
|
|
3425
|
+
selectionNeedsScroll = true;
|
|
2805
3426
|
tui.requestRender();
|
|
2806
3427
|
};
|
|
2807
3428
|
const requestStop = () => {
|
|
@@ -2810,16 +3431,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2810
3431
|
stopRequested = true;
|
|
2811
3432
|
stopStatus = undefined;
|
|
2812
3433
|
setWorkflowStatus(undefined);
|
|
2813
|
-
const selectedOption = options[selectedIndex];
|
|
2814
3434
|
void runAction(`stop ${store.runId}`, true, (status) => {
|
|
2815
3435
|
stopStatus = status;
|
|
2816
3436
|
setWorkflowStatus(status);
|
|
2817
3437
|
if (!disposed)
|
|
2818
3438
|
tui.requestRender();
|
|
2819
|
-
}).then(() => updateDashboard(
|
|
3439
|
+
}).then(() => updateDashboard()).catch((error) => {
|
|
2820
3440
|
if (disposed)
|
|
2821
3441
|
return;
|
|
2822
3442
|
stopStatus = `Could not stop workflow ${store.runId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3443
|
+
setWorkflowStatus(stopStatus);
|
|
2823
3444
|
tui.requestRender();
|
|
2824
3445
|
}).finally(() => {
|
|
2825
3446
|
stopRequested = false;
|
|
@@ -2831,129 +3452,203 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2831
3452
|
if (refreshing || stopRequested)
|
|
2832
3453
|
return;
|
|
2833
3454
|
refreshing = true;
|
|
2834
|
-
|
|
2835
|
-
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3455
|
+
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
2836
3456
|
}, 1000);
|
|
2837
3457
|
timer.unref();
|
|
2838
|
-
return
|
|
3458
|
+
return {
|
|
2839
3459
|
render(width) {
|
|
3460
|
+
renderedWidth = width;
|
|
3461
|
+
const narrow = width < 80;
|
|
2840
3462
|
const styles = themeWorkflowProgressStyles(theme);
|
|
2841
|
-
const
|
|
2842
|
-
const
|
|
2843
|
-
const
|
|
2844
|
-
const
|
|
2845
|
-
const
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3463
|
+
const agent = selectedAgentRecord();
|
|
3464
|
+
const actions = actionMode ? { title: agent ? "Agent actions" : "Run actions", options: actionOptions(), index: actionIndex } : undefined;
|
|
3465
|
+
const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { nodeId: selectedNodeId, expandedNodeIds: [...expandedNodeIds], ...(narrow && !detailsMode ? { treeOnly: true } : {}), ...(narrow && detailsMode ? { detailsOnly: true } : {}), ...(actions ? { actions } : {}) }, styles);
|
|
3466
|
+
const statusLines = stopStatus ? truncateToVisualLines(styles.error(stopStatus), Number.MAX_SAFE_INTEGER, width, 0).visualLines.map((line) => line.trimEnd()) : [];
|
|
3467
|
+
const content = [...statusLines, ...phaseLines];
|
|
3468
|
+
const rows = terminalRows();
|
|
3469
|
+
const hintRows = rows >= 3 ? 1 : 0;
|
|
3470
|
+
const viewport = Math.max(1, rows - hintRows);
|
|
3471
|
+
const maxOffset = Math.max(0, content.length - viewport);
|
|
3472
|
+
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
3473
|
+
if (actionMode) {
|
|
3474
|
+
const label = actions?.options[actionIndex];
|
|
3475
|
+
const actionRow = label ? content.findIndex((line) => line.includes(label)) : -1;
|
|
3476
|
+
if (actionRow >= 0) {
|
|
3477
|
+
if (actionRow < dashboardOffset)
|
|
3478
|
+
dashboardOffset = actionRow;
|
|
3479
|
+
else if (actionRow >= dashboardOffset + viewport)
|
|
3480
|
+
dashboardOffset = actionRow - viewport + 1;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
else if (!detailsMode && selectionNeedsScroll) {
|
|
3484
|
+
const selectedRow = content.findIndex((line) => line.startsWith("→"));
|
|
3485
|
+
if (selectedRow >= 0) {
|
|
3486
|
+
if (selectedRow < dashboardOffset)
|
|
3487
|
+
dashboardOffset = selectedRow;
|
|
3488
|
+
else if (selectedRow >= dashboardOffset + viewport)
|
|
3489
|
+
dashboardOffset = selectedRow - viewport + 1;
|
|
3490
|
+
}
|
|
3491
|
+
selectionNeedsScroll = false;
|
|
2850
3492
|
}
|
|
2851
|
-
const maxOffset = Math.max(0, dashboardLines.length - layout.dashboardViewport);
|
|
2852
3493
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
2853
|
-
const
|
|
2854
|
-
return [
|
|
2855
|
-
...dashboardLines.slice(dashboardOffset, dashboardOffset + layout.dashboardViewport),
|
|
2856
|
-
...(layout.separatorRows && layout.dashboardViewport ? [""] : []),
|
|
2857
|
-
...actionLines.slice(actionStart, actionStart + layout.actionViewport),
|
|
2858
|
-
...(layout.hintRows ? [hint] : []),
|
|
2859
|
-
];
|
|
3494
|
+
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
3495
|
+
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
2860
3496
|
},
|
|
2861
3497
|
invalidate() { },
|
|
2862
3498
|
handleInput(data) {
|
|
2863
|
-
if (stopRequested)
|
|
3499
|
+
if (stopRequested || editorRunning)
|
|
3500
|
+
return;
|
|
3501
|
+
const narrow = renderedWidth < 80;
|
|
3502
|
+
if (!actionMode && (data === "a" || data === "A")) {
|
|
3503
|
+
actionMode = true;
|
|
3504
|
+
actionIndex = 0;
|
|
3505
|
+
dashboardOffset = 0;
|
|
3506
|
+
tui.requestRender();
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
if (actionMode) {
|
|
3510
|
+
const options = actionOptions();
|
|
3511
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3512
|
+
actionMode = false;
|
|
3513
|
+
dashboardOffset = 0;
|
|
3514
|
+
tui.requestRender();
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3518
|
+
actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
3519
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3520
|
+
actionIndex = (actionIndex + 1) % options.length;
|
|
3521
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3522
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3523
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3524
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3525
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3526
|
+
const action = options[actionIndex];
|
|
3527
|
+
const agent = selectedAgentRecord();
|
|
3528
|
+
if (!action || action === "Back") {
|
|
3529
|
+
actionMode = false;
|
|
3530
|
+
dashboardOffset = 0;
|
|
3531
|
+
}
|
|
3532
|
+
else if (agent) {
|
|
3533
|
+
const worktree = agentWorktreeFor(view, agent);
|
|
3534
|
+
if (action === "Open result in editor") {
|
|
3535
|
+
const result = view.agentResults.get(agent.id);
|
|
3536
|
+
if (result !== undefined)
|
|
3537
|
+
void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
|
|
3538
|
+
}
|
|
3539
|
+
else if (action === "Copy agent ID")
|
|
3540
|
+
void copyArtifact(agent.id, "agent ID");
|
|
3541
|
+
else if (action === "Copy branch" && worktree)
|
|
3542
|
+
void copyArtifact(worktree.branch, "branch");
|
|
3543
|
+
else if (action === "Copy worktree path" && worktree)
|
|
3544
|
+
void copyArtifact(worktree.path, "worktree path");
|
|
3545
|
+
else
|
|
3546
|
+
done(`__workflow_fork__:${agent.id}`);
|
|
3547
|
+
}
|
|
3548
|
+
else if (action === "Open script in editor")
|
|
3549
|
+
void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
|
|
3550
|
+
else if (action === "Stop")
|
|
3551
|
+
requestStop();
|
|
3552
|
+
else
|
|
3553
|
+
done(action);
|
|
3554
|
+
}
|
|
3555
|
+
tui.requestRender();
|
|
2864
3556
|
return;
|
|
2865
|
-
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
const currentIndex = Math.max(0, phases.findIndex((phase) => phase.id === selectedPhaseId));
|
|
2872
|
-
const delta = left ? -1 : 1;
|
|
2873
|
-
const nextPhase = phases[(currentIndex + delta + phases.length) % phases.length];
|
|
2874
|
-
selectedPhaseId = nextPhase?.id;
|
|
2875
|
-
selectedAgentId = nextPhase?.agents[0]?.id;
|
|
3557
|
+
}
|
|
3558
|
+
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3559
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3560
|
+
if (narrow && detailsMode) {
|
|
3561
|
+
detailsMode = false;
|
|
3562
|
+
selectionNeedsScroll = true;
|
|
2876
3563
|
}
|
|
3564
|
+
else
|
|
3565
|
+
done("Back");
|
|
2877
3566
|
}
|
|
2878
|
-
else if (
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3567
|
+
else if (narrow && detailsMode) {
|
|
3568
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3569
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3570
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3571
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3572
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3573
|
+
if (current?.kind === "agent" && current.agentId) {
|
|
3574
|
+
actionMode = true;
|
|
3575
|
+
actionIndex = 0;
|
|
3576
|
+
}
|
|
3577
|
+
else if (current?.children.length) {
|
|
3578
|
+
if (expandedNodeIds.has(current.id))
|
|
3579
|
+
expandedNodeIds.delete(current.id);
|
|
3580
|
+
else
|
|
3581
|
+
expandedNodeIds.add(current.id);
|
|
3582
|
+
}
|
|
2883
3583
|
}
|
|
2884
3584
|
}
|
|
2885
|
-
else if (keybindings
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, dashboardLayout().dashboardViewport));
|
|
3585
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
3586
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
3587
|
+
selectedNodeId = next.nodeId;
|
|
3588
|
+
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3589
|
+
selectionNeedsScroll = true;
|
|
2891
3590
|
}
|
|
2892
|
-
else if (keybindings
|
|
2893
|
-
|
|
3591
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
3592
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
3593
|
+
selectedNodeId = next.nodeId;
|
|
3594
|
+
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3595
|
+
selectionNeedsScroll = true;
|
|
2894
3596
|
}
|
|
2895
|
-
else if (keybindings
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3597
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
3598
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
3599
|
+
selectedNodeId = next.nodeId;
|
|
3600
|
+
selectionNeedsScroll = true;
|
|
3601
|
+
}
|
|
3602
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
3603
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
3604
|
+
selectedNodeId = next.nodeId;
|
|
3605
|
+
selectionNeedsScroll = true;
|
|
3606
|
+
}
|
|
3607
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3608
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3609
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3610
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3611
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3612
|
+
if (narrow)
|
|
3613
|
+
detailsMode = true;
|
|
3614
|
+
else if (current?.kind === "agent" && current.agentId) {
|
|
3615
|
+
actionMode = true;
|
|
3616
|
+
actionIndex = 0;
|
|
3617
|
+
}
|
|
3618
|
+
else if (current?.children.length) {
|
|
3619
|
+
if (expandedNodeIds.has(current.id))
|
|
3620
|
+
expandedNodeIds.delete(current.id);
|
|
3621
|
+
else
|
|
3622
|
+
expandedNodeIds.add(current.id);
|
|
3623
|
+
}
|
|
2900
3624
|
}
|
|
2901
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
2902
|
-
done(undefined);
|
|
2903
3625
|
tui.requestRender();
|
|
2904
3626
|
},
|
|
2905
3627
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "
|
|
2909
|
-
if (!actionChoice || actionChoice === "
|
|
2910
|
-
|
|
3628
|
+
};
|
|
3629
|
+
})
|
|
3630
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
3631
|
+
if (!actionChoice || actionChoice === "Back") {
|
|
3632
|
+
stores = await loadStores();
|
|
3633
|
+
break;
|
|
3634
|
+
}
|
|
2911
3635
|
if (actionChoice === "Agents...") {
|
|
2912
3636
|
await selectAgent(view);
|
|
2913
3637
|
continue;
|
|
2914
3638
|
}
|
|
2915
|
-
if (actionChoice
|
|
3639
|
+
if (actionChoice.startsWith("__workflow_agent__:")) {
|
|
3640
|
+
await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
|
|
2916
3641
|
continue;
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
const move = (delta) => {
|
|
2924
|
-
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
2925
|
-
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
2926
|
-
};
|
|
2927
|
-
return borderWorkflowOverlay({
|
|
2928
|
-
render(width) {
|
|
2929
|
-
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
2930
|
-
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
2931
|
-
offset = Math.min(offset, maxOffset);
|
|
2932
|
-
return [
|
|
2933
|
-
theme.fg("accent", "Workflow script"),
|
|
2934
|
-
...renderedLines.slice(offset, offset + viewport()),
|
|
2935
|
-
"",
|
|
2936
|
-
theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close"),
|
|
2937
|
-
];
|
|
2938
|
-
},
|
|
2939
|
-
invalidate() { },
|
|
2940
|
-
handleInput(data) {
|
|
2941
|
-
if (keybindings.matches(data, "tui.select.up"))
|
|
2942
|
-
move(-1);
|
|
2943
|
-
else if (keybindings.matches(data, "tui.select.down"))
|
|
2944
|
-
move(1);
|
|
2945
|
-
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
2946
|
-
move(-viewport());
|
|
2947
|
-
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
2948
|
-
move(viewport());
|
|
2949
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
2950
|
-
done(undefined);
|
|
2951
|
-
tui.requestRender();
|
|
2952
|
-
},
|
|
2953
|
-
}, theme);
|
|
2954
|
-
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3642
|
+
}
|
|
3643
|
+
if (actionChoice.startsWith("__workflow_fork__:")) {
|
|
3644
|
+
const agentId = actionChoice.slice("__workflow_fork__:".length);
|
|
3645
|
+
const agent = view.agents.find((candidate) => candidate.id === agentId);
|
|
3646
|
+
if (agent)
|
|
3647
|
+
await forkAgentSession(view, agent);
|
|
2955
3648
|
continue;
|
|
2956
3649
|
}
|
|
3650
|
+
if (actionChoice === "Refresh")
|
|
3651
|
+
continue;
|
|
2957
3652
|
const copy = view.copies.get(actionChoice);
|
|
2958
3653
|
if (copy) {
|
|
2959
3654
|
await copyArtifact(copy.value, copy.artifact);
|
|
@@ -2988,7 +3683,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2988
3683
|
const currentLayout = layout();
|
|
2989
3684
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
2990
3685
|
offset = Math.min(offset, maxOffset);
|
|
2991
|
-
const
|
|
3686
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3687
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3688
|
+
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
2992
3689
|
const controls = currentLayout.compactControls
|
|
2993
3690
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
2994
3691
|
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
@@ -3002,22 +3699,22 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3002
3699
|
},
|
|
3003
3700
|
invalidate() { },
|
|
3004
3701
|
handleInput(data) {
|
|
3005
|
-
if (keybindings
|
|
3702
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3006
3703
|
selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
3007
|
-
else if (keybindings
|
|
3704
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3008
3705
|
selectedIndex = (selectedIndex + 1) % options.length;
|
|
3009
|
-
else if (keybindings
|
|
3706
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3010
3707
|
move(-layout().contentViewport);
|
|
3011
|
-
else if (keybindings
|
|
3708
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3012
3709
|
move(layout().contentViewport);
|
|
3013
|
-
else if (keybindings
|
|
3710
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
|
|
3014
3711
|
done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
|
|
3015
|
-
else if (keybindings
|
|
3712
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
|
|
3016
3713
|
done(undefined);
|
|
3017
3714
|
tui.requestRender();
|
|
3018
3715
|
},
|
|
3019
3716
|
}, theme);
|
|
3020
|
-
}, { overlay: true, overlayOptions:
|
|
3717
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS });
|
|
3021
3718
|
if (decision) {
|
|
3022
3719
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3023
3720
|
if (!accepted)
|
|
@@ -3044,16 +3741,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3044
3741
|
pi.on("session_shutdown", async () => {
|
|
3045
3742
|
try {
|
|
3046
3743
|
await Promise.all([...runs.entries()].map(async ([runId, run]) => {
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
3051
|
-
if (!["completed", "failed", "stopped", "budget_exhausted"].includes(run.lifecycle.state)) {
|
|
3744
|
+
const isTerminal = SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state);
|
|
3745
|
+
if (!isTerminal) {
|
|
3052
3746
|
try {
|
|
3053
3747
|
await run.lifecycle.terminal("interrupted");
|
|
3054
3748
|
}
|
|
3055
3749
|
catch (error) {
|
|
3056
|
-
if (!
|
|
3750
|
+
if (!SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
3057
3751
|
throw error;
|
|
3058
3752
|
}
|
|
3059
3753
|
run.abortController.abort();
|