pi-extensible-workflows 3.1.0 → 3.2.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 -18
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +44 -4
- package/dist/src/host.js +843 -395
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/validation.js +34 -0
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -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/execution.ts +13 -4
- package/src/host.ts +702 -362
- package/src/index.ts +1 -0
- package/src/types.ts +1 -1
- package/src/validation.ts +27 -0
- package/src/workflow-artifacts.ts +34 -0
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";
|
|
18
|
+
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
|
|
17
19
|
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, 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,7 +86,6 @@ 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
|
}
|
|
87
91
|
export class RunLifecycle {
|
|
@@ -144,7 +148,7 @@ export class RunLifecycle {
|
|
|
144
148
|
await this.enter();
|
|
145
149
|
}
|
|
146
150
|
async terminal(state, reason) {
|
|
147
|
-
if (
|
|
151
|
+
if (HARD_TERMINAL_RUN_STATES.has(this.#state))
|
|
148
152
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `${this.#state} runs are terminal`);
|
|
149
153
|
await this.#set(state, reason ?? state);
|
|
150
154
|
for (const resolve of this.#waiters.splice(0))
|
|
@@ -165,18 +169,40 @@ export function formatWorkflowPreview(args) {
|
|
|
165
169
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
166
170
|
}
|
|
167
171
|
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
|
|
172
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
173
|
+
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. Foreground results include the completed run ID. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId }) replays a failed run into a child; workflow_resume({ runId, budget? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
174
|
+
function workflowRecoveryGuidance(action, state) {
|
|
175
|
+
if (action === "resume") {
|
|
176
|
+
if (state === "failed")
|
|
177
|
+
return "Failed workflow runs must use workflow_retry({ runId })";
|
|
178
|
+
if (state === "completed")
|
|
179
|
+
return "Completed workflow runs have no recovery action";
|
|
180
|
+
if (state === "stopped")
|
|
181
|
+
return "Stopped workflow runs have no recovery action; launch a new workflow";
|
|
182
|
+
if (state === "interrupted")
|
|
183
|
+
return "Interrupted workflow runs use /workflow resume, not workflow_resume";
|
|
184
|
+
return `Only budget-exhausted runs can be resumed with workflow_resume; source is ${state}`;
|
|
185
|
+
}
|
|
186
|
+
if (state === "budget_exhausted")
|
|
187
|
+
return "Budget-exhausted workflow runs must use workflow_resume({ runId, budget? })";
|
|
188
|
+
if (state === "completed")
|
|
189
|
+
return "Completed workflow runs have no recovery action";
|
|
190
|
+
if (state === "stopped")
|
|
191
|
+
return "Stopped workflow runs cannot be retried; launch a new workflow";
|
|
192
|
+
if (state === "interrupted")
|
|
193
|
+
return "Interrupted workflow runs use /workflow resume, not workflow_retry";
|
|
194
|
+
return `Only failed workflow runs can be retried; source is ${state}`;
|
|
195
|
+
}
|
|
170
196
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
171
|
-
name: Type.Optional(Type.String({ description: "Required non-empty name for inline workflow
|
|
197
|
+
name: Type.Optional(Type.String({ description: "Required non-empty name for the default inline workflow path; invalid for registered function launches" })),
|
|
172
198
|
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: "
|
|
199
|
+
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(...)" })),
|
|
200
|
+
workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name" })),
|
|
175
201
|
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: "
|
|
202
|
+
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
|
|
203
|
+
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
|
|
204
|
+
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
205
|
+
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
180
206
|
});
|
|
181
207
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
182
208
|
function phaseNames(source) {
|
|
@@ -258,7 +284,7 @@ export function buildWorkflowPhaseModel(run, source) {
|
|
|
258
284
|
const agents = observation?.agents ?? [];
|
|
259
285
|
const counts = phaseAgentCounts(agents);
|
|
260
286
|
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,
|
|
287
|
+
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
|
|
262
288
|
});
|
|
263
289
|
let currentPhaseIndex;
|
|
264
290
|
for (let index = phases.length - 1; index >= 0; index -= 1) {
|
|
@@ -282,12 +308,197 @@ export function buildWorkflowPhaseModel(run, source) {
|
|
|
282
308
|
result.unassignedAgents = unassignedAgents;
|
|
283
309
|
return result;
|
|
284
310
|
}
|
|
311
|
+
function workflowPhaseTreePath(kind, phaseId, operationPath, agentId) {
|
|
312
|
+
const root = `phase/${encodeURIComponent(phaseId)}`;
|
|
313
|
+
if (kind === "phase")
|
|
314
|
+
return root;
|
|
315
|
+
const operation = operationPath.map((part) => encodeURIComponent(part)).join("/");
|
|
316
|
+
if (kind === "operation")
|
|
317
|
+
return `${root}/operation/${operation}`;
|
|
318
|
+
return operation ? `${root}/operation/${operation}/agent/${encodeURIComponent(agentId ?? "")}` : `${root}/agent/${encodeURIComponent(agentId ?? "")}`;
|
|
319
|
+
}
|
|
320
|
+
function workflowPhaseTreeAggregateState(states) {
|
|
321
|
+
if (!states.length || states.every((state) => state === "completed"))
|
|
322
|
+
return "completed";
|
|
323
|
+
if (states.some((state) => state === "failed"))
|
|
324
|
+
return "failed";
|
|
325
|
+
if (states.some((state) => state === "cancelled"))
|
|
326
|
+
return "cancelled";
|
|
327
|
+
if (states.some((state) => state === "running"))
|
|
328
|
+
return "running";
|
|
329
|
+
return "queued";
|
|
330
|
+
}
|
|
331
|
+
export function buildWorkflowPhaseTree(model) {
|
|
332
|
+
const drafts = new Map();
|
|
333
|
+
const roots = [];
|
|
334
|
+
const add = (node, parentId) => {
|
|
335
|
+
const existing = drafts.get(node.id);
|
|
336
|
+
if (existing)
|
|
337
|
+
return existing;
|
|
338
|
+
const draft = { ...node, ...(parentId === undefined ? {} : { parentId }), children: [] };
|
|
339
|
+
drafts.set(draft.id, draft);
|
|
340
|
+
if (parentId === undefined)
|
|
341
|
+
roots.push(draft.id);
|
|
342
|
+
else
|
|
343
|
+
drafts.get(parentId)?.children.push(draft.id);
|
|
344
|
+
return draft;
|
|
345
|
+
};
|
|
346
|
+
const samePath = (left, right) => left.length === right.length && left.every((part, index) => part === right[index]);
|
|
347
|
+
const addPhase = (phaseId, label, agents, phase) => {
|
|
348
|
+
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 } : {}) });
|
|
349
|
+
const operationNodes = new Map();
|
|
350
|
+
const entries = agents.map((agent) => ({ agent, path: [...(agent.structuralPath ?? [])], node: undefined, defaultParentId: phaseNode.id }));
|
|
351
|
+
const agentEntries = new Map(entries.map((entry) => [entry.agent.id, entry]));
|
|
352
|
+
const acceptedParents = new Map();
|
|
353
|
+
const wouldCycle = (childId, parentId) => {
|
|
354
|
+
const seen = new Set([childId]);
|
|
355
|
+
let current = parentId;
|
|
356
|
+
while (current) {
|
|
357
|
+
if (seen.has(current))
|
|
358
|
+
return true;
|
|
359
|
+
seen.add(current);
|
|
360
|
+
current = acceptedParents.get(current);
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
};
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
const parent = entry.agent.parentId ? agentEntries.get(entry.agent.parentId) : undefined;
|
|
366
|
+
if (parent && !wouldCycle(entry.agent.id, parent.agent.id))
|
|
367
|
+
acceptedParents.set(entry.agent.id, parent.agent.id);
|
|
368
|
+
}
|
|
369
|
+
const operationChain = (path, owner, startIndex = 0) => {
|
|
370
|
+
let parent = owner;
|
|
371
|
+
for (let index = startIndex; index < path.length; index += 1) {
|
|
372
|
+
const prefix = path.slice(0, index + 1);
|
|
373
|
+
const key = `${owner.id}:${JSON.stringify(prefix)}`;
|
|
374
|
+
const existing = operationNodes.get(key);
|
|
375
|
+
if (existing) {
|
|
376
|
+
parent = existing;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const suffix = path.slice(startIndex, index + 1).map((part) => encodeURIComponent(part)).join("/");
|
|
380
|
+
const id = owner.id === phaseNode.id ? workflowPhaseTreePath("operation", phaseId, prefix) : `${owner.id}/operation/${suffix}`;
|
|
381
|
+
const operation = add({ id, kind: "operation", label: prefix.at(-1) ?? "", depth: 0, phaseId, operationPath: prefix, state: "queued", ...(phase ? { phase } : {}) }, parent.id);
|
|
382
|
+
operationNodes.set(key, operation);
|
|
383
|
+
parent = operation;
|
|
384
|
+
}
|
|
385
|
+
return parent;
|
|
386
|
+
};
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
if (!acceptedParents.has(entry.agent.id))
|
|
389
|
+
entry.defaultParentId = operationChain(entry.path, phaseNode).id;
|
|
390
|
+
}
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
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);
|
|
393
|
+
}
|
|
394
|
+
const attach = (entry, parentId) => {
|
|
395
|
+
const previous = entry.node.parentId ? drafts.get(entry.node.parentId) : undefined;
|
|
396
|
+
if (previous)
|
|
397
|
+
previous.children = previous.children.filter((childId) => childId !== entry.node.id);
|
|
398
|
+
entry.node.parentId = parentId;
|
|
399
|
+
const parent = drafts.get(parentId);
|
|
400
|
+
if (parent && !parent.children.includes(entry.node.id))
|
|
401
|
+
parent.children.push(entry.node.id);
|
|
402
|
+
};
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
const parentId = acceptedParents.get(entry.agent.id);
|
|
405
|
+
const parent = parentId ? agentEntries.get(parentId) : undefined;
|
|
406
|
+
if (parent) {
|
|
407
|
+
if (samePath(entry.path, parent.path))
|
|
408
|
+
entry.defaultParentId = parent.node.id;
|
|
409
|
+
else {
|
|
410
|
+
const commonLength = entry.path.findIndex((part, index) => parent.path[index] !== part);
|
|
411
|
+
const startIndex = commonLength < 0 ? Math.min(entry.path.length, parent.path.length) : commonLength;
|
|
412
|
+
entry.defaultParentId = operationChain(entry.path, parent.node, startIndex).id;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
attach(entry, entry.defaultParentId);
|
|
416
|
+
}
|
|
417
|
+
const setDepth = (node, depth, seen = new Set()) => {
|
|
418
|
+
if (seen.has(node.id))
|
|
419
|
+
return;
|
|
420
|
+
seen.add(node.id);
|
|
421
|
+
node.depth = depth;
|
|
422
|
+
for (const childId of node.children) {
|
|
423
|
+
const child = drafts.get(childId);
|
|
424
|
+
if (child)
|
|
425
|
+
setDepth(child, depth + 1, seen);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
setDepth(phaseNode, 0);
|
|
429
|
+
const statesFor = (node, seen = new Set()) => {
|
|
430
|
+
if (seen.has(node.id))
|
|
431
|
+
return [];
|
|
432
|
+
const nextSeen = new Set(seen).add(node.id);
|
|
433
|
+
return node.children.flatMap((childId) => {
|
|
434
|
+
const child = drafts.get(childId);
|
|
435
|
+
return child?.kind === "agent" ? [child.agent?.state ?? "queued", ...statesFor(child, nextSeen)] : child ? statesFor(child, nextSeen) : [];
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
for (const operation of operationNodes.values())
|
|
439
|
+
operation.state = workflowPhaseTreeAggregateState(statesFor(operation));
|
|
440
|
+
};
|
|
441
|
+
for (const phase of model.phases)
|
|
442
|
+
addPhase(phase.id, `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`, phase.agents, phase);
|
|
443
|
+
if (model.unassignedAgents?.length)
|
|
444
|
+
addPhase("unassigned", "Unassigned", model.unassignedAgents);
|
|
445
|
+
const nodes = [...drafts.values()].map((node) => ({ ...node, children: [...node.children] }));
|
|
446
|
+
return { roots, nodes, byId: new Map(nodes.map((node) => [node.id, node])) };
|
|
447
|
+
}
|
|
448
|
+
export function workflowPhaseTreeVisibleNodes(tree, expanded = new Set()) {
|
|
449
|
+
const visible = [];
|
|
450
|
+
const visit = (id) => {
|
|
451
|
+
const node = tree.byId.get(id);
|
|
452
|
+
if (!node)
|
|
453
|
+
return;
|
|
454
|
+
visible.push(node);
|
|
455
|
+
if (expanded.has(node.id))
|
|
456
|
+
for (const childId of node.children)
|
|
457
|
+
visit(childId);
|
|
458
|
+
};
|
|
459
|
+
for (const root of tree.roots)
|
|
460
|
+
visit(root);
|
|
461
|
+
return visible;
|
|
462
|
+
}
|
|
463
|
+
export function workflowPhaseTreeInitialExpanded(tree) {
|
|
464
|
+
return new Set(tree.nodes.filter((node) => node.children.length > 0).map((node) => node.id));
|
|
465
|
+
}
|
|
466
|
+
export function preserveWorkflowPhaseTreeSelection(tree, selection) {
|
|
467
|
+
const node = (selection.nodeId ? tree.byId.get(selection.nodeId) : undefined) ?? tree.nodes[0];
|
|
468
|
+
return node ? { nodeId: node.id } : {};
|
|
469
|
+
}
|
|
470
|
+
export function navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, direction) {
|
|
471
|
+
const expanded = new Set(expandedNodeIds);
|
|
472
|
+
const current = (selectedNodeId ? tree.byId.get(selectedNodeId) : undefined) ?? tree.nodes[0];
|
|
473
|
+
if (!current)
|
|
474
|
+
return { expandedNodeIds: expanded };
|
|
475
|
+
if (direction === "left") {
|
|
476
|
+
if (current.children.length && expanded.delete(current.id))
|
|
477
|
+
return { nodeId: current.id, expandedNodeIds: expanded };
|
|
478
|
+
return { nodeId: current.parentId ?? current.id, expandedNodeIds: expanded };
|
|
479
|
+
}
|
|
480
|
+
if (direction === "right") {
|
|
481
|
+
if (current.children.length && !expanded.has(current.id)) {
|
|
482
|
+
expanded.add(current.id);
|
|
483
|
+
return { nodeId: current.id, expandedNodeIds: expanded };
|
|
484
|
+
}
|
|
485
|
+
return { nodeId: current.children[0] ?? current.id, expandedNodeIds: expanded };
|
|
486
|
+
}
|
|
487
|
+
const visible = workflowPhaseTreeVisibleNodes(tree, expanded);
|
|
488
|
+
const index = Math.max(0, visible.findIndex((node) => node.id === current.id));
|
|
489
|
+
const next = visible[(index + (direction === "up" ? visible.length - 1 : 1)) % visible.length];
|
|
490
|
+
return { nodeId: next?.id ?? current.id, expandedNodeIds: expanded };
|
|
491
|
+
}
|
|
285
492
|
export function preserveWorkflowPhaseSelection(model, selection) {
|
|
286
493
|
const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
|
|
287
494
|
if (!phase)
|
|
288
|
-
return {};
|
|
289
|
-
const
|
|
290
|
-
|
|
495
|
+
return model.unassignedAgents?.length ? { nodeId: workflowPhaseTreePath("phase", "unassigned", []) } : {};
|
|
496
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
497
|
+
const selectedAgent = selection.agentId ? phase.agents.find((candidate) => candidate.id === selection.agentId) : undefined;
|
|
498
|
+
const selectedCandidate = selection.nodeId ? tree.byId.get(selection.nodeId) : undefined;
|
|
499
|
+
const selected = selectedCandidate?.phaseId === phase.id ? selectedCandidate : selectedAgent ? tree.byId.get(workflowPhaseTreePath("agent", phase.id, selectedAgent.structuralPath ?? [], selectedAgent.id)) : undefined;
|
|
500
|
+
const nodeId = selected?.id ?? tree.byId.get(workflowPhaseTreePath("phase", phase.id, []))?.id;
|
|
501
|
+
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
502
|
}
|
|
292
503
|
function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
293
504
|
function agentGroupLabel(agents) {
|
|
@@ -322,26 +533,31 @@ function renderGroupedAgents(agents, render, allAgents = agents, groupLabel = (l
|
|
|
322
533
|
...group.entries.map((entry) => render(entry, grouped)),
|
|
323
534
|
]);
|
|
324
535
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
536
|
+
const RUN_STATE_GLYPH = { completed: "✓", failed: "✗", stopped: "✗", budget_exhausted: "!", awaiting_input: "●" };
|
|
537
|
+
const AGENT_STATE_GLYPH = { completed: "✓", failed: "✗", cancelled: "✗" };
|
|
538
|
+
function runStateGlyph(state, running) { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
539
|
+
function agentStateGlyph(state, running) { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
|
|
540
|
+
const PROGRESS_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", running: "accent" };
|
|
541
|
+
const WORKFLOW_ICON_STYLE = { completed: "success", failed: "error", stopped: "error", budget_exhausted: "warning", running: "accent" };
|
|
542
|
+
const PHASE_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", running: "accent", interrupted: "warning", budget_exhausted: "warning" };
|
|
543
|
+
function styleForState(map, state, styles) {
|
|
544
|
+
const key = map[state] ?? "muted";
|
|
545
|
+
return (text) => styles[key](text);
|
|
546
|
+
}
|
|
547
|
+
function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
548
|
+
function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
549
|
+
function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
334
550
|
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
335
551
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
336
|
-
const workflowIcon = run.state
|
|
337
|
-
const
|
|
552
|
+
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
553
|
+
const iconStyle = workflowIconStyle(run.state, styles);
|
|
338
554
|
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
339
|
-
const lines = [`${
|
|
555
|
+
const lines = [`${iconStyle(workflowIcon)} ${header}`];
|
|
340
556
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
341
557
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
342
558
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
343
559
|
const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
344
|
-
const icon = agent.state
|
|
560
|
+
const icon = agentStateGlyph(agent.state, spinner);
|
|
345
561
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
346
562
|
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
347
563
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
@@ -392,6 +608,103 @@ function workflowCatalogBlock(text, expanded) {
|
|
|
392
608
|
invalidate() { },
|
|
393
609
|
};
|
|
394
610
|
}
|
|
611
|
+
function controlString(value) { return typeof value === "string" && value.trim() ? value : undefined; }
|
|
612
|
+
function controlValue(value) {
|
|
613
|
+
if (value === null)
|
|
614
|
+
return "removed";
|
|
615
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
616
|
+
return String(value);
|
|
617
|
+
const json = JSON.stringify(value);
|
|
618
|
+
return typeof json === "string" ? json : "unknown";
|
|
619
|
+
}
|
|
620
|
+
function controlTitle(name, theme) { return theme.fg("toolTitle", theme.bold(name)); }
|
|
621
|
+
function controlState(state, theme) {
|
|
622
|
+
const color = state === "completed" || state === "running" || state === "stopped" ? "success" : state === "failed" || state === "unknown" ? "error" : state === "budget_exhausted" || state === "awaiting_approval" ? "warning" : "accent";
|
|
623
|
+
return theme.fg(color, state);
|
|
624
|
+
}
|
|
625
|
+
function controlAction(action, theme) {
|
|
626
|
+
const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
627
|
+
return theme.fg(color, action);
|
|
628
|
+
}
|
|
629
|
+
function budgetPatchEntries(value) {
|
|
630
|
+
if (!object(value))
|
|
631
|
+
return value === undefined ? [] : [controlValue(value)];
|
|
632
|
+
return Object.entries(value).map(([dimension, limits]) => {
|
|
633
|
+
if (limits === null)
|
|
634
|
+
return `${dimension}=removed`;
|
|
635
|
+
if (!object(limits))
|
|
636
|
+
return `${dimension}=${controlValue(limits)}`;
|
|
637
|
+
const parts = ["soft", "hard"].filter((key) => Object.prototype.hasOwnProperty.call(limits, key)).map((key) => `${key}=${controlValue(limits[key])}`);
|
|
638
|
+
return `${dimension} ${parts.join(" ")}`;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function budgetPatchSummary(value) {
|
|
642
|
+
const entries = budgetPatchEntries(value);
|
|
643
|
+
return entries.length ? entries.join(", ") : "unchanged";
|
|
644
|
+
}
|
|
645
|
+
function budgetPatchDetails(value, theme) {
|
|
646
|
+
const entries = budgetPatchEntries(value);
|
|
647
|
+
return entries.length ? [theme.fg("accent", theme.bold("Budget patch")), ...entries.map((entry) => ` ${theme.fg("toolOutput", entry)}`)] : [];
|
|
648
|
+
}
|
|
649
|
+
function workflowControlValue(result) { return catalogResultValue(result); }
|
|
650
|
+
function workflowControlCall(name, args, theme) {
|
|
651
|
+
const runId = controlString(args.runId) ?? "(missing run ID)";
|
|
652
|
+
if (name === "workflow_respond") {
|
|
653
|
+
const proposalId = controlString(args.proposalId);
|
|
654
|
+
const target = proposalId ? `budget proposal ${proposalId}` : `checkpoint ${controlString(args.name) ?? "(missing name)"}`;
|
|
655
|
+
const decision = args.approved === true ? "approve" : "reject";
|
|
656
|
+
return [`${controlTitle(name, theme)} ${theme.fg("accent", runId)}`, `${theme.fg("muted", target)} · ${controlAction(decision, theme)}`].join("\n");
|
|
657
|
+
}
|
|
658
|
+
if (name === "workflow_resume")
|
|
659
|
+
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");
|
|
660
|
+
if (name === "workflow_retry")
|
|
661
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)} ${theme.fg("muted", "failed run")}`;
|
|
662
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)}`;
|
|
663
|
+
}
|
|
664
|
+
function workflowControlResult(name, args, result, expanded, theme, isError) {
|
|
665
|
+
if (isError) {
|
|
666
|
+
const text = result.content?.filter(({ type }) => type === "text").map(({ text }) => text ?? "").join("\n").trim();
|
|
667
|
+
return theme.fg("error", text || `The ${name} tool failed.`);
|
|
668
|
+
}
|
|
669
|
+
const value = workflowControlValue(result);
|
|
670
|
+
if (!object(value))
|
|
671
|
+
return theme.fg("error", `The ${name} tool returned an invalid result.`);
|
|
672
|
+
const runId = controlString(args.runId) ?? controlString(value.runId) ?? "(unknown)";
|
|
673
|
+
const title = controlTitle(name, theme);
|
|
674
|
+
if (name === "workflow_stop") {
|
|
675
|
+
const state = controlString(value.state) ?? "unknown";
|
|
676
|
+
const action = value.stopped === true ? "stopped" : value.reason === "already_terminal" ? "already terminal" : value.reason === "unknown_run" ? "run not found" : "no change";
|
|
677
|
+
if (!expanded)
|
|
678
|
+
return `${title}\nRun ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`;
|
|
679
|
+
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");
|
|
680
|
+
}
|
|
681
|
+
if (name === "workflow_retry") {
|
|
682
|
+
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
683
|
+
const state = controlString(value.state) ?? "unknown";
|
|
684
|
+
if (!expanded)
|
|
685
|
+
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction("started", theme)}`].join("\n");
|
|
686
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction("started; completed work will be replayed", theme)}`].join("\n");
|
|
687
|
+
}
|
|
688
|
+
if (name === "workflow_resume") {
|
|
689
|
+
const state = controlString(value.state) ?? "unknown";
|
|
690
|
+
const proposalId = controlString(value.proposalId);
|
|
691
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
|
|
692
|
+
if (!expanded)
|
|
693
|
+
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
694
|
+
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");
|
|
695
|
+
}
|
|
696
|
+
const proposalId = controlString(args.proposalId);
|
|
697
|
+
const checkpointName = controlString(args.name);
|
|
698
|
+
const target = proposalId ? `Budget proposal ${theme.fg("accent", proposalId)}` : `Checkpoint ${theme.fg("accent", checkpointName ?? "(missing)")}`;
|
|
699
|
+
const accepted = value.accepted === true;
|
|
700
|
+
const approved = value.approved === true;
|
|
701
|
+
const reason = controlString(value.reason);
|
|
702
|
+
const action = reason === "proposal_not_pending" ? "not pending" : reason === "checkpoint" && !accepted ? "not pending" : approved ? "approved" : "rejected";
|
|
703
|
+
const state = controlString(value.state);
|
|
704
|
+
if (!expanded)
|
|
705
|
+
return [title, target, `Run ${theme.fg("accent", runId)} · ${controlAction(action, theme)}${state ? ` · ${controlState(state, theme)}` : ""}`].join("\n");
|
|
706
|
+
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");
|
|
707
|
+
}
|
|
395
708
|
function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
|
|
396
709
|
function catalogResultValue(result) {
|
|
397
710
|
if (result.details !== undefined)
|
|
@@ -468,7 +781,8 @@ function formatWorkflowCatalog(value, expanded, theme) {
|
|
|
468
781
|
return theme.fg("error", value.error.message);
|
|
469
782
|
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
470
783
|
}
|
|
471
|
-
const
|
|
784
|
+
const ANSI_SGR_SOURCE = `${String.fromCharCode(27)}\\[[0-9;]*m`;
|
|
785
|
+
const ANSI_SGR = new RegExp(ANSI_SGR_SOURCE);
|
|
472
786
|
export function truncateWorkflowProgress(text, width) {
|
|
473
787
|
const safeWidth = Math.max(1, width);
|
|
474
788
|
return text.split("\n").flatMap((line) => {
|
|
@@ -539,7 +853,7 @@ function navigatorRunLabels(entries) {
|
|
|
539
853
|
nameCount.set(run.workflowName, (nameCount.get(run.workflowName) ?? 0) + 1);
|
|
540
854
|
return entries.map(({ store, loaded: { run } }) => {
|
|
541
855
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
542
|
-
const glyph = run.state
|
|
856
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
543
857
|
const suffix = (nameCount.get(run.workflowName) ?? 0) > 1 ? ` ${store.runId.slice(0, 8)}` : "";
|
|
544
858
|
const cost = run.agents.reduce((sum, a) => sum + (a.accounting?.cost ?? 0), 0);
|
|
545
859
|
const costStr = cost > 0 ? ` $${cost.toFixed(2)}` : "";
|
|
@@ -587,7 +901,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
587
901
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
588
902
|
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
903
|
const hasAccounting = run.agents.some((a) => a.accounting);
|
|
590
|
-
const glyph = run.state
|
|
904
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
591
905
|
const header = `${glyph} ${run.workflowName}`;
|
|
592
906
|
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
907
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
@@ -598,7 +912,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
598
912
|
lines.push("");
|
|
599
913
|
const byId = new Map(run.agents.map((a) => [a.id, a]));
|
|
600
914
|
const render = ({ agent, depth }, grouped) => {
|
|
601
|
-
const icon = agent.state
|
|
915
|
+
const icon = agentStateGlyph(agent.state, "⠦");
|
|
602
916
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
603
917
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
604
918
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
@@ -621,7 +935,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
621
935
|
}
|
|
622
936
|
return lines.join("\n");
|
|
623
937
|
}
|
|
624
|
-
export function formatNavigatorRun(loaded, checkpoints,
|
|
938
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
625
939
|
const { run, snapshot } = loaded;
|
|
626
940
|
const lines = [
|
|
627
941
|
`Workflow: ${run.workflowName}`,
|
|
@@ -664,23 +978,79 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
664
978
|
lines.push(" (none)");
|
|
665
979
|
for (const checkpoint of checkpoints)
|
|
666
980
|
lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
667
|
-
lines.push(`Worktrees: ${String(
|
|
981
|
+
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
668
982
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
669
983
|
return lines.join("\n");
|
|
670
984
|
}
|
|
671
985
|
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
672
986
|
const safeWidth = Math.max(1, width);
|
|
673
987
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
988
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
989
|
+
const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
|
|
674
990
|
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
|
|
991
|
+
// ponytail: ANSI-only width, good enough for the ASCII labels the tree renders
|
|
992
|
+
const ansiPattern = new RegExp(ANSI_SGR_SOURCE, "g");
|
|
993
|
+
const visibleLength = (text) => text.replace(ansiPattern, "").length;
|
|
994
|
+
const padTo = (text, limit) => `${text}${" ".repeat(Math.max(0, limit - visibleLength(text)))}`;
|
|
995
|
+
const phaseStyle = (state) => phaseStyleForState(state, styles);
|
|
996
|
+
const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
|
|
997
|
+
const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
|
|
998
|
+
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];
|
|
999
|
+
const selectedPhase = selectedNode?.phase ?? (selectedNode ? model.phases.find((candidate) => candidate.id === selectedNode.phaseId) : undefined);
|
|
1000
|
+
const visibleNodes = workflowPhaseTreeVisibleNodes(tree, expanded);
|
|
1001
|
+
const nodeAgents = (node) => {
|
|
1002
|
+
const agents = [];
|
|
1003
|
+
const visit = (id) => {
|
|
1004
|
+
const child = tree.byId.get(id);
|
|
1005
|
+
if (!child)
|
|
1006
|
+
return;
|
|
1007
|
+
if (child.agent)
|
|
1008
|
+
agents.push(child.agent);
|
|
1009
|
+
else
|
|
1010
|
+
for (const childId of child.children)
|
|
1011
|
+
visit(childId);
|
|
1012
|
+
};
|
|
1013
|
+
if (node.agent)
|
|
1014
|
+
agents.push(node.agent);
|
|
1015
|
+
else
|
|
1016
|
+
for (const childId of node.children)
|
|
1017
|
+
visit(childId);
|
|
1018
|
+
return agents;
|
|
1019
|
+
};
|
|
1020
|
+
const nodeStatus = (node) => phaseStyle(node.state)(node.state);
|
|
1021
|
+
const nodeIcon = (node) => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : " ";
|
|
1022
|
+
const treeLine = (node) => {
|
|
1023
|
+
const selected = node.id === selectedNode?.id;
|
|
1024
|
+
const state = progressStyleForState(node.state, styles);
|
|
1025
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}`;
|
|
1026
|
+
};
|
|
1027
|
+
const details = (node) => {
|
|
1028
|
+
if (!node)
|
|
1029
|
+
return [styles.muted("No workflow node is selected")];
|
|
1030
|
+
const agents = nodeAgents(node);
|
|
1031
|
+
if (node.kind === "phase") {
|
|
1032
|
+
const selected = node.phase;
|
|
1033
|
+
const counts = selected?.counts ?? phaseAgentCounts(agents);
|
|
1034
|
+
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)}`];
|
|
1035
|
+
}
|
|
1036
|
+
if (node.kind === "operation") {
|
|
1037
|
+
const states = phaseAgentCounts(agents);
|
|
1038
|
+
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)}`];
|
|
1039
|
+
}
|
|
1040
|
+
const agent = node.agent;
|
|
1041
|
+
if (!agent)
|
|
1042
|
+
return [styles.muted("Agent details are unavailable")];
|
|
1043
|
+
const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
|
|
1044
|
+
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)}`, ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
1045
|
+
const error = agent.attemptDetails?.at(-1)?.error;
|
|
1046
|
+
if (error)
|
|
1047
|
+
result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
1048
|
+
if (agent.activity)
|
|
1049
|
+
result.push(`Activity: ${agent.activity.text}`);
|
|
1050
|
+
return result;
|
|
1051
|
+
};
|
|
679
1052
|
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
680
1053
|
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
1054
|
const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
685
1055
|
if (run.error)
|
|
686
1056
|
lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
@@ -688,46 +1058,32 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
688
1058
|
for (const event of run.events ?? [])
|
|
689
1059
|
lines.push(styles.warning(`Warning: ${event.message}`));
|
|
690
1060
|
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;
|
|
1061
|
+
const renderTree = (limit) => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
|
|
1062
|
+
const actionRows = () => {
|
|
1063
|
+
const actions = selection.actions;
|
|
1064
|
+
if (!actions)
|
|
1065
|
+
return [];
|
|
1066
|
+
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
708
1067
|
};
|
|
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];
|
|
1068
|
+
const detailRows = () => [...details(selectedNode), ...actionRows()];
|
|
714
1069
|
if (safeWidth >= 80) {
|
|
715
|
-
const sidebarWidth = Math.min(
|
|
1070
|
+
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
716
1071
|
const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
|
|
717
|
-
const sidebar =
|
|
718
|
-
const detail =
|
|
1072
|
+
const sidebar = renderTree(sidebarWidth).flatMap((line) => wrap(line, sidebarWidth));
|
|
1073
|
+
const detail = detailRows().flatMap((line) => wrap(line, detailWidth));
|
|
719
1074
|
const rows = Math.max(sidebar.length, detail.length);
|
|
720
1075
|
for (let index = 0; index < rows; index += 1)
|
|
721
|
-
lines.push(`${sidebar[index] ?? ""} | ${detail[index] ?? ""}`);
|
|
1076
|
+
lines.push(`${padTo(sidebar[index] ?? "", sidebarWidth)} | ${detail[index] ?? ""}`);
|
|
1077
|
+
}
|
|
1078
|
+
else if (selection.detailsOnly) {
|
|
1079
|
+
lines.push(...detailRows().flatMap((line) => wrap(line)));
|
|
722
1080
|
}
|
|
723
1081
|
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)));
|
|
1082
|
+
lines.push(...renderTree(safeWidth));
|
|
1083
|
+
if (!selection.treeOnly)
|
|
1084
|
+
lines.push("", ...detailRows().flatMap((line) => wrap(line)));
|
|
729
1085
|
}
|
|
730
|
-
if (model.unassignedAgents?.length)
|
|
1086
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
|
|
731
1087
|
lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
732
1088
|
return lines.flatMap((line) => wrap(line));
|
|
733
1089
|
}
|
|
@@ -1216,23 +1572,22 @@ function projectTrusted(ctx) {
|
|
|
1216
1572
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
1217
1573
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
1218
1574
|
}
|
|
1219
|
-
function
|
|
1575
|
+
function asFn(value) { return typeof value === "function" ? value : undefined; }
|
|
1220
1576
|
function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
|
|
1221
1577
|
function piHostCapabilities(pi) {
|
|
1222
1578
|
if (!object(pi))
|
|
1223
1579
|
return {};
|
|
1224
|
-
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
1580
|
+
const registerEntryRenderer = asFn(pi.registerEntryRenderer);
|
|
1225
1581
|
const events = pi.events;
|
|
1226
|
-
return { ...(
|
|
1582
|
+
return { ...(registerEntryRenderer ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
1227
1583
|
}
|
|
1228
|
-
function isModelRegistryGetter(value) { return typeof value === "function"; }
|
|
1229
1584
|
function contextHostCapabilities(ctx) {
|
|
1230
1585
|
if (!object(ctx) || !object(ctx.modelRegistry))
|
|
1231
1586
|
return {};
|
|
1232
1587
|
const registry = ctx.modelRegistry;
|
|
1233
|
-
const getAll = registry.getAll;
|
|
1234
|
-
const getAvailable = registry.getAvailable;
|
|
1235
|
-
return { modelRegistry: { ...(
|
|
1588
|
+
const getAll = asFn(registry.getAll);
|
|
1589
|
+
const getAvailable = asFn(registry.getAvailable);
|
|
1590
|
+
return { modelRegistry: { ...(getAll ? { getAll: () => getAll.call(registry) } : {}), ...(getAvailable ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
1236
1591
|
}
|
|
1237
1592
|
function modelInventory(root, registry) {
|
|
1238
1593
|
const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
@@ -1266,24 +1621,21 @@ async function resolveLaunchAliases(registry, staticAliases, context, availableM
|
|
|
1266
1621
|
throw error;
|
|
1267
1622
|
}
|
|
1268
1623
|
}
|
|
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
1624
|
function uiHostCapabilities(ui) {
|
|
1273
1625
|
if (!object(ui))
|
|
1274
1626
|
return undefined;
|
|
1275
|
-
const select = ui.select;
|
|
1276
|
-
const input = ui.input;
|
|
1277
|
-
const setStatus = ui.setStatus;
|
|
1278
|
-
return { ...(
|
|
1627
|
+
const select = asFn(ui.select);
|
|
1628
|
+
const input = asFn(ui.input);
|
|
1629
|
+
const setStatus = asFn(ui.setStatus);
|
|
1630
|
+
return { ...(select ? { select } : {}), ...(input ? { input } : {}), ...(setStatus ? { setStatus } : {}) };
|
|
1279
1631
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
1632
|
+
function tuiRows(tui) {
|
|
1633
|
+
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1634
|
+
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1284
1635
|
}
|
|
1285
|
-
function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
|
|
1286
1636
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1637
|
+
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1638
|
+
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
|
|
1287
1639
|
function borderWorkflowOverlay(component, theme) {
|
|
1288
1640
|
return {
|
|
1289
1641
|
...component,
|
|
@@ -1293,13 +1645,10 @@ function borderWorkflowOverlay(component, theme) {
|
|
|
1293
1645
|
},
|
|
1294
1646
|
};
|
|
1295
1647
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return {};
|
|
1300
|
-
return { getKeys: keybindings.getKeys };
|
|
1648
|
+
function keybindingKeys(keybindings, name) {
|
|
1649
|
+
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
|
|
1650
|
+
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1301
1651
|
}
|
|
1302
|
-
function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
|
|
1303
1652
|
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
|
|
1304
1653
|
beginWorkflowExtensionLoading();
|
|
1305
1654
|
const registry = loadingRegistry();
|
|
@@ -1558,7 +1907,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1558
1907
|
catch {
|
|
1559
1908
|
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
1909
|
}
|
|
1561
|
-
|
|
1910
|
+
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
1911
|
+
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 } : {}) };
|
|
1562
1912
|
});
|
|
1563
1913
|
return { ...current, agents };
|
|
1564
1914
|
});
|
|
@@ -1567,7 +1917,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1567
1917
|
});
|
|
1568
1918
|
const cleanupTerminalRun = async (runId) => {
|
|
1569
1919
|
const run = runs.get(runId);
|
|
1570
|
-
if (!run || !
|
|
1920
|
+
if (!run || !HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
1571
1921
|
return;
|
|
1572
1922
|
await scheduler.cancelRun(runId);
|
|
1573
1923
|
await scheduler.flush();
|
|
@@ -1705,6 +2055,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1705
2055
|
throw mainAgentError(error);
|
|
1706
2056
|
}
|
|
1707
2057
|
},
|
|
2058
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_respond", args, theme)); },
|
|
2059
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_respond", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
1708
2060
|
});
|
|
1709
2061
|
pi.registerTool({
|
|
1710
2062
|
name: "workflow_stop",
|
|
@@ -1720,6 +2072,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1720
2072
|
throw mainAgentError(error);
|
|
1721
2073
|
}
|
|
1722
2074
|
},
|
|
2075
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_stop", args, theme)); },
|
|
2076
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_stop", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
1723
2077
|
});
|
|
1724
2078
|
let catalogRegistered = false;
|
|
1725
2079
|
let sessionStarted = false;
|
|
@@ -1751,30 +2105,78 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1751
2105
|
});
|
|
1752
2106
|
catalogRegistered = true;
|
|
1753
2107
|
};
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2108
|
+
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
2109
|
+
const activeSnapshotTools = (tools, active) => active === "session"
|
|
2110
|
+
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
2111
|
+
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
2112
|
+
const resumeLaunchPrologue = async (input) => {
|
|
2113
|
+
const active = new Set(pi.getActiveTools().filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
2114
|
+
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1758
2115
|
if (missing)
|
|
1759
2116
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1760
2117
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1761
|
-
const
|
|
1762
|
-
const
|
|
1763
|
-
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2118
|
+
const resolution = resolveWorkflowSettings(input.cwd, input.trustedProject, settingsPath);
|
|
2119
|
+
const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
|
|
1764
2120
|
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1765
|
-
const previousAliases =
|
|
2121
|
+
const previousAliases = input.snapshot.modelAliases ?? input.snapshot.settings.modelAliases ?? {};
|
|
2122
|
+
const inventory = modelInventory(input.rootModel, input.modelRegistry);
|
|
2123
|
+
const knownModels = input.modelRegistry ? inventory.knownModels : new Set([...input.snapshot.models, ...inventory.knownModels]);
|
|
2124
|
+
const availableModels = input.modelRegistry ? inventory.availableModels : new Set([...input.snapshot.models, ...inventory.availableModels]);
|
|
2125
|
+
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;
|
|
2126
|
+
const blockedAliases = input.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2127
|
+
const blockedAliasTargets = input.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2128
|
+
let script;
|
|
2129
|
+
if (input.withPreflight) {
|
|
2130
|
+
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2131
|
+
script = launchScriptForSnapshot(input.snapshot, registry);
|
|
2132
|
+
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);
|
|
2133
|
+
}
|
|
2134
|
+
const refreshed = resumedSnapshotSettings(input.snapshot, resolution, currentAliases);
|
|
2135
|
+
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2136
|
+
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
2137
|
+
};
|
|
2138
|
+
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => async (prompt, options, agentSignal, identity) => {
|
|
2139
|
+
await lifecycle.enter();
|
|
2140
|
+
try {
|
|
2141
|
+
const path = agentIdentityPath(identity);
|
|
2142
|
+
const replayed = await store.replay(path);
|
|
2143
|
+
if (replayed) {
|
|
2144
|
+
return replayed.value;
|
|
2145
|
+
}
|
|
2146
|
+
const worktree = agentWorktree(identity);
|
|
2147
|
+
const agentCwd = worktree.worktreeOwner ? (await persistWorktree(store, metadata, worktree.worktreeOwner)).cwd : cwd;
|
|
2148
|
+
const role = typeof options.role === "string" ? options.role : undefined;
|
|
2149
|
+
const model = typeof options.model === "string" ? options.model : undefined;
|
|
2150
|
+
const thinking = parseThinking(options.thinking);
|
|
2151
|
+
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
2152
|
+
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
2153
|
+
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
2154
|
+
const tools = resolved.tools;
|
|
2155
|
+
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
2156
|
+
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 });
|
|
2157
|
+
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
2158
|
+
if (agentSignal.aborted)
|
|
2159
|
+
cancel();
|
|
2160
|
+
else
|
|
2161
|
+
agentSignal.addEventListener("abort", cancel, { once: true });
|
|
2162
|
+
const outcome = await spawned.result.finally(() => { agentSignal.removeEventListener("abort", cancel); });
|
|
2163
|
+
if (!outcome.ok)
|
|
2164
|
+
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
2165
|
+
await store.complete(path, outcome.value);
|
|
2166
|
+
return outcome.value;
|
|
2167
|
+
}
|
|
2168
|
+
finally {
|
|
2169
|
+
await lifecycle.leave();
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
const refreshPausedRunAliases = async (run, context) => {
|
|
2173
|
+
const loaded = await run.store.load();
|
|
2174
|
+
const trustedProject = context?.projectTrusted ?? run.projectTrusted();
|
|
1766
2175
|
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 });
|
|
2176
|
+
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
2177
|
await run.store.saveSnapshot(snapshot);
|
|
1776
2178
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1777
|
-
run.executor =
|
|
2179
|
+
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
2180
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
1779
2181
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1780
2182
|
if (drift.length)
|
|
@@ -1793,19 +2195,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1793
2195
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
1794
2196
|
if (missingRole)
|
|
1795
2197
|
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
2198
|
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
2199
|
const controller = new AbortController();
|
|
1810
2200
|
if (context?.signal?.aborted)
|
|
1811
2201
|
controller.abort();
|
|
@@ -1813,17 +2203,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1813
2203
|
context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
|
|
1814
2204
|
}
|
|
1815
2205
|
run.abortController = controller;
|
|
1816
|
-
const currentAliases
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const blockedAliasTargets = context?.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1820
|
-
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
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 });
|
|
2206
|
+
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 });
|
|
2207
|
+
if (!script)
|
|
2208
|
+
throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
1824
2209
|
await run.store.saveSnapshot(snapshot);
|
|
1825
2210
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1826
|
-
run.executor =
|
|
2211
|
+
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
2212
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1828
2213
|
if (drift.length)
|
|
1829
2214
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
@@ -1835,7 +2220,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1835
2220
|
}
|
|
1836
2221
|
catch (error) {
|
|
1837
2222
|
const typed = asWorkflowError(error);
|
|
1838
|
-
if (!
|
|
2223
|
+
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
|
|
1839
2224
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
1840
2225
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
|
|
1841
2226
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
@@ -1848,37 +2233,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1848
2233
|
}
|
|
1849
2234
|
await scheduler.cancelRun(run.store.runId);
|
|
1850
2235
|
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);
|
|
2236
|
+
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, 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);
|
|
1882
2237
|
run.execution = execution;
|
|
1883
2238
|
const completion = execution.result.then(async (value) => {
|
|
1884
2239
|
await scheduler.flush();
|
|
@@ -1924,11 +2279,26 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1924
2279
|
};
|
|
1925
2280
|
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
1926
2281
|
const run = runs.get(runId);
|
|
1927
|
-
if (!run)
|
|
1928
|
-
|
|
2282
|
+
if (!run) {
|
|
2283
|
+
const host = object(context) ? context : {};
|
|
2284
|
+
const cwd = typeof host.cwd === "string" ? host.cwd : undefined;
|
|
2285
|
+
const sessionManager = object(host.sessionManager) ? host.sessionManager : undefined;
|
|
2286
|
+
const sessionId = typeof sessionManager?.getSessionId === "function" ? String(Reflect.apply(sessionManager.getSessionId, sessionManager, [])) : undefined;
|
|
2287
|
+
if (cwd && sessionId) {
|
|
2288
|
+
try {
|
|
2289
|
+
const state = (await new RunStore(cwd, sessionId, runId, home).load()).run.state;
|
|
2290
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", state));
|
|
2291
|
+
}
|
|
2292
|
+
catch (error) {
|
|
2293
|
+
if (error instanceof WorkflowError)
|
|
2294
|
+
throw error;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session`);
|
|
2298
|
+
}
|
|
1929
2299
|
const loaded = await run.store.load();
|
|
1930
2300
|
if (loaded.run.state !== "budget_exhausted")
|
|
1931
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "
|
|
2301
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", loaded.run.state));
|
|
1932
2302
|
const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1933
2303
|
const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
|
|
1934
2304
|
const nextBudget = mergeBudget(currentBudget, patch);
|
|
@@ -1973,10 +2343,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1973
2343
|
loaded = await sourceStore.load();
|
|
1974
2344
|
}
|
|
1975
2345
|
catch (error) {
|
|
1976
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `
|
|
2346
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session: ${errorText(error)}`);
|
|
1977
2347
|
}
|
|
1978
2348
|
if (loaded.run.state !== "failed")
|
|
1979
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE",
|
|
2349
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("retry", loaded.run.state));
|
|
1980
2350
|
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
2351
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "The source retry provenance is incomplete");
|
|
1982
2352
|
const lineageRootRunId = loaded.run.retry?.lineageRootRunId ?? loaded.run.id;
|
|
@@ -2012,28 +2382,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2012
2382
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
2013
2383
|
if (missingRole)
|
|
2014
2384
|
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
2385
|
const modelRegistry = contextHostCapabilities(context).modelRegistry;
|
|
2025
2386
|
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
2387
|
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);
|
|
2388
|
+
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
2389
|
await sourceStore.validateNamedWorktrees();
|
|
2038
2390
|
for (const name of loaded.run.retry?.namedWorktrees ?? [])
|
|
2039
2391
|
await sourceStore.resolveNamedWorktree(name);
|
|
@@ -2043,8 +2395,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2043
2395
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
2044
2396
|
const childRunId = randomUUID();
|
|
2045
2397
|
const childStore = new RunStore(cwd, sessionId, childRunId, home);
|
|
2046
|
-
const
|
|
2047
|
-
const childSnapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2398
|
+
const childSnapshot = childBaseSnapshot;
|
|
2048
2399
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
2049
2400
|
const childInitialBudget = childBudget.snapshot();
|
|
2050
2401
|
const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
@@ -2055,7 +2406,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2055
2406
|
const abortController = new AbortController();
|
|
2056
2407
|
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
2057
2408
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2058
|
-
const childRun = { executor:
|
|
2409
|
+
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
2410
|
runs.set(childRunId, childRun);
|
|
2060
2411
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2061
2412
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
@@ -2086,6 +2437,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2086
2437
|
throw mainAgentError(error);
|
|
2087
2438
|
}
|
|
2088
2439
|
},
|
|
2440
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_retry", args, theme)); },
|
|
2441
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_retry", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
2089
2442
|
});
|
|
2090
2443
|
pi.registerTool({
|
|
2091
2444
|
name: "workflow_resume",
|
|
@@ -2101,6 +2454,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2101
2454
|
throw mainAgentError(error);
|
|
2102
2455
|
}
|
|
2103
2456
|
},
|
|
2457
|
+
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_resume", args, theme)); },
|
|
2458
|
+
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult("workflow_resume", context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
2104
2459
|
});
|
|
2105
2460
|
pi.on("session_start", async (_event, ctx) => {
|
|
2106
2461
|
if (sessionStarted)
|
|
@@ -2143,7 +2498,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2143
2498
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
2144
2499
|
const abortController = new AbortController();
|
|
2145
2500
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
2146
|
-
runs.set(runId, { executor:
|
|
2501
|
+
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
2502
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
2148
2503
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
2149
2504
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -2205,7 +2560,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2205
2560
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
2206
2561
|
const knownModels = inventory.knownModels;
|
|
2207
2562
|
const availableModels = inventory.availableModels;
|
|
2208
|
-
const rootTools = pi.getActiveTools().filter((name) => !
|
|
2563
|
+
const rootTools = pi.getActiveTools().filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
2209
2564
|
const trustedProject = projectTrusted(ctx);
|
|
2210
2565
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
2211
2566
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -2242,45 +2597,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2242
2597
|
const providerPause = async () => { if (background)
|
|
2243
2598
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2244
2599
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
2245
|
-
const executor =
|
|
2600
|
+
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
2601
|
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
2602
|
if (params.foreground && onUpdate)
|
|
2248
2603
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2249
2604
|
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);
|
|
2605
|
+
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), 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
2606
|
runs.get(runId).execution = execution;
|
|
2285
2607
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2286
2608
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2398,7 +2720,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2398
2720
|
return keepContext ? "dashboard" : "done";
|
|
2399
2721
|
}
|
|
2400
2722
|
if (action === "delete" && stored) {
|
|
2401
|
-
if (!
|
|
2723
|
+
if (!HARD_TERMINAL_RUN_STATES.has(stored.loaded.run.state)) {
|
|
2402
2724
|
ctx.ui.notify("Stop the workflow before deleting it.", "warning");
|
|
2403
2725
|
return keepContext ? "dashboard" : "done";
|
|
2404
2726
|
}
|
|
@@ -2591,7 +2913,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2591
2913
|
}
|
|
2592
2914
|
const sorted = navigatorAttentionSort(stores);
|
|
2593
2915
|
const labels = navigatorRunLabels(sorted);
|
|
2594
|
-
const terminalStates =
|
|
2916
|
+
const terminalStates = HARD_TERMINAL_RUN_STATES;
|
|
2595
2917
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
2596
2918
|
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
2597
2919
|
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
@@ -2661,6 +2983,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2661
2983
|
const loaded = await store.load();
|
|
2662
2984
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2663
2985
|
const worktrees = await store.worktrees();
|
|
2986
|
+
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
2987
|
+
const agentResults = new Map();
|
|
2988
|
+
for (const agent of loaded.run.agents) {
|
|
2989
|
+
if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
|
|
2990
|
+
continue;
|
|
2991
|
+
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
2992
|
+
if (operation)
|
|
2993
|
+
agentResults.set(agent.id, operation.value);
|
|
2994
|
+
}
|
|
2664
2995
|
const actions = new Map();
|
|
2665
2996
|
const copies = new Map();
|
|
2666
2997
|
const reviews = new Map();
|
|
@@ -2695,8 +3026,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2695
3026
|
if (ctx.mode !== "tui")
|
|
2696
3027
|
actions.set("Refresh", "refresh");
|
|
2697
3028
|
else
|
|
2698
|
-
actions.set("
|
|
2699
|
-
if (loaded.run.agents.length)
|
|
3029
|
+
actions.set("Open script in editor", "open-script");
|
|
3030
|
+
if (ctx.mode !== "tui" && loaded.run.agents.length)
|
|
2700
3031
|
actions.set("Agents...", "agents");
|
|
2701
3032
|
if (terminalStates.has(loaded.run.state))
|
|
2702
3033
|
add("Delete", "delete");
|
|
@@ -2704,31 +3035,55 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2704
3035
|
addCopy("Copy run path", store.directory, "run path");
|
|
2705
3036
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2706
3037
|
}
|
|
2707
|
-
return { dashboard: formatWorkflowPhaseDashboard(loaded.run, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(loaded.run, loaded.snapshot), run: loaded.run, snapshot: loaded.snapshot, actions, copies, reviews,
|
|
3038
|
+
return { dashboard: formatWorkflowPhaseDashboard(loaded.run, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(loaded.run, loaded.snapshot), run: loaded.run, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
|
|
3039
|
+
};
|
|
3040
|
+
const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
3041
|
+
const agentActionLabels = (dashboard, agent) => {
|
|
3042
|
+
const worktree = agentWorktreeFor(dashboard, agent);
|
|
3043
|
+
return [
|
|
3044
|
+
...(agent.attemptDetails?.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3045
|
+
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3046
|
+
...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
|
|
3047
|
+
"Copy agent ID",
|
|
3048
|
+
"Back",
|
|
3049
|
+
];
|
|
2708
3050
|
};
|
|
2709
|
-
const
|
|
3051
|
+
const forkAgentSession = async (dashboard, agent) => {
|
|
3052
|
+
const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3053
|
+
const worktree = agentWorktreeFor(dashboard, agent);
|
|
3054
|
+
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3055
|
+
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
3056
|
+
const index = choice ? choices.indexOf(choice) : -1;
|
|
3057
|
+
const attempt = index >= 0 ? attempts[index] : undefined;
|
|
3058
|
+
if (!attempt)
|
|
3059
|
+
return;
|
|
3060
|
+
const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
3061
|
+
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?"))
|
|
3062
|
+
return;
|
|
3063
|
+
try {
|
|
3064
|
+
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
3065
|
+
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
3066
|
+
}
|
|
3067
|
+
catch (error) {
|
|
3068
|
+
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
const selectAgent = async (dashboard, requestedAgentId) => {
|
|
2710
3072
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
2711
3073
|
const title = (agent) => agentBreadcrumb(agent, byId, true);
|
|
2712
3074
|
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3075
|
+
let selected;
|
|
3076
|
+
if (requestedAgentId)
|
|
3077
|
+
selected = dashboard.agents.find((agent) => agent.id === requestedAgentId);
|
|
3078
|
+
else {
|
|
3079
|
+
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
3080
|
+
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
3081
|
+
selected = selectedIndex >= 0 ? dashboard.agents[selectedIndex] : undefined;
|
|
3082
|
+
}
|
|
2716
3083
|
if (!selected)
|
|
2717
3084
|
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
|
-
};
|
|
3085
|
+
const worktree = agentWorktreeFor(dashboard, selected);
|
|
3086
|
+
const actions = agentActionLabels(dashboard, selected);
|
|
2732
3087
|
for (;;) {
|
|
2733
3088
|
const action = await ctx.ui.select(title(selected), actions);
|
|
2734
3089
|
if (!action || action === "Back")
|
|
@@ -2745,63 +3100,87 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2745
3100
|
await copyArtifact(worktree.path, "worktree path");
|
|
2746
3101
|
continue;
|
|
2747
3102
|
}
|
|
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
|
-
}
|
|
3103
|
+
if (action === "Fork as Pi session in pane")
|
|
3104
|
+
await forkAgentSession(dashboard, selected);
|
|
2763
3105
|
}
|
|
2764
3106
|
};
|
|
2765
3107
|
for (;;) {
|
|
2766
3108
|
let view = await loadDashboard();
|
|
2767
3109
|
const actionChoice = ctx.mode === "tui"
|
|
2768
3110
|
? await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
2769
|
-
let options = [...view.actions.keys(), "Close"];
|
|
2770
|
-
let selectedIndex = 0;
|
|
2771
3111
|
let dashboardOffset = 0;
|
|
2772
3112
|
let refreshing = false;
|
|
2773
3113
|
let disposed = false;
|
|
3114
|
+
let detailsMode = false;
|
|
3115
|
+
let actionMode = false;
|
|
3116
|
+
let actionIndex = 0;
|
|
2774
3117
|
let stopRequested = false;
|
|
2775
3118
|
let stopStatus;
|
|
3119
|
+
let selectionNeedsScroll = true;
|
|
3120
|
+
let renderedWidth = 80;
|
|
3121
|
+
let refreshGeneration = 0;
|
|
2776
3122
|
const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
|
|
2777
|
-
let
|
|
2778
|
-
let
|
|
3123
|
+
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3124
|
+
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
3125
|
+
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
2779
3126
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
2780
|
-
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→",
|
|
3127
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
2781
3128
|
const keyLabel = (binding, fallback) => {
|
|
2782
3129
|
const keys = keybindingKeys(keybindings, binding);
|
|
2783
3130
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
2784
3131
|
};
|
|
2785
|
-
const
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
const
|
|
2791
|
-
return
|
|
3132
|
+
const selectedAgentRecord = () => {
|
|
3133
|
+
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3134
|
+
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
3135
|
+
};
|
|
3136
|
+
const actionOptions = () => {
|
|
3137
|
+
const agent = selectedAgentRecord();
|
|
3138
|
+
return agent ? agentActionLabels(view, agent) : [...view.actions.keys(), "Back"];
|
|
3139
|
+
};
|
|
3140
|
+
let editorRunning = false;
|
|
3141
|
+
const openArtifact = async (artifact, label) => {
|
|
3142
|
+
if (editorRunning)
|
|
3143
|
+
return;
|
|
3144
|
+
editorRunning = true;
|
|
3145
|
+
try {
|
|
3146
|
+
const command = SettingsManager.create(view.cwd, extensionAgentDir, { projectTrusted: projectTrusted(ctx) }).getExternalEditorCommand();
|
|
3147
|
+
if (!command) {
|
|
3148
|
+
ctx.ui.notify(`Cannot open ${label}: no external editor is configured.`, "warning");
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
const exitCode = await openWorkflowArtifact(tui, command, await artifact);
|
|
3152
|
+
if (exitCode !== 0) {
|
|
3153
|
+
const detail = exitCode === null ? "could not be started" : `exited with code ${String(exitCode)}`;
|
|
3154
|
+
ctx.ui.notify(`Cannot open ${label}: external editor ${detail}.`, "warning");
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
catch (error) {
|
|
3158
|
+
ctx.ui.notify(`Cannot open ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3159
|
+
}
|
|
3160
|
+
finally {
|
|
3161
|
+
editorRunning = false;
|
|
3162
|
+
tui.requestRender(true);
|
|
3163
|
+
}
|
|
2792
3164
|
};
|
|
2793
|
-
const updateDashboard = async (
|
|
2794
|
-
const
|
|
2795
|
-
const
|
|
3165
|
+
const updateDashboard = async () => {
|
|
3166
|
+
const generation = ++refreshGeneration;
|
|
3167
|
+
const hadExpandableNodes = tree.nodes.some((node) => node.children.length > 0);
|
|
2796
3168
|
const next = await loadDashboard();
|
|
2797
|
-
if (disposed)
|
|
3169
|
+
if (disposed || generation !== refreshGeneration)
|
|
2798
3170
|
return;
|
|
3171
|
+
const previousNodeId = selectedNodeId;
|
|
3172
|
+
const previousExpanded = expandedNodeIds;
|
|
3173
|
+
const selectedAction = actionMode ? actionOptions()[actionIndex] : undefined;
|
|
2799
3174
|
view = next;
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3175
|
+
tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3176
|
+
selectedNodeId = preserveWorkflowPhaseTreeSelection(tree, { nodeId: previousNodeId }).nodeId;
|
|
3177
|
+
expandedNodeIds = new Set([...previousExpanded].filter((id) => tree.byId.has(id)));
|
|
3178
|
+
if (!hadExpandableNodes && !expandedNodeIds.size && tree.nodes.some((node) => node.children.length > 0))
|
|
3179
|
+
expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
3180
|
+
const nextActions = actionOptions();
|
|
3181
|
+
const preservedActionIndex = selectedAction ? nextActions.indexOf(selectedAction) : -1;
|
|
3182
|
+
actionIndex = preservedActionIndex >= 0 ? preservedActionIndex : selectedAction ? nextActions.length - 1 : Math.min(actionIndex, Math.max(0, nextActions.length - 1));
|
|
3183
|
+
selectionNeedsScroll = true;
|
|
2805
3184
|
tui.requestRender();
|
|
2806
3185
|
};
|
|
2807
3186
|
const requestStop = () => {
|
|
@@ -2810,16 +3189,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2810
3189
|
stopRequested = true;
|
|
2811
3190
|
stopStatus = undefined;
|
|
2812
3191
|
setWorkflowStatus(undefined);
|
|
2813
|
-
const selectedOption = options[selectedIndex];
|
|
2814
3192
|
void runAction(`stop ${store.runId}`, true, (status) => {
|
|
2815
3193
|
stopStatus = status;
|
|
2816
3194
|
setWorkflowStatus(status);
|
|
2817
3195
|
if (!disposed)
|
|
2818
3196
|
tui.requestRender();
|
|
2819
|
-
}).then(() => updateDashboard(
|
|
3197
|
+
}).then(() => updateDashboard()).catch((error) => {
|
|
2820
3198
|
if (disposed)
|
|
2821
3199
|
return;
|
|
2822
3200
|
stopStatus = `Could not stop workflow ${store.runId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3201
|
+
setWorkflowStatus(stopStatus);
|
|
2823
3202
|
tui.requestRender();
|
|
2824
3203
|
}).finally(() => {
|
|
2825
3204
|
stopRequested = false;
|
|
@@ -2831,80 +3210,181 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2831
3210
|
if (refreshing || stopRequested)
|
|
2832
3211
|
return;
|
|
2833
3212
|
refreshing = true;
|
|
2834
|
-
|
|
2835
|
-
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3213
|
+
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
2836
3214
|
}, 1000);
|
|
2837
3215
|
timer.unref();
|
|
2838
3216
|
return borderWorkflowOverlay({
|
|
2839
3217
|
render(width) {
|
|
3218
|
+
renderedWidth = width;
|
|
3219
|
+
const narrow = width < 80;
|
|
2840
3220
|
const styles = themeWorkflowProgressStyles(theme);
|
|
2841
|
-
const
|
|
2842
|
-
const
|
|
2843
|
-
const
|
|
2844
|
-
const
|
|
2845
|
-
const
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3221
|
+
const agent = selectedAgentRecord();
|
|
3222
|
+
const actions = actionMode ? { title: agent ? "Agent actions" : "Run actions", options: actionOptions(), index: actionIndex } : undefined;
|
|
3223
|
+
const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { nodeId: selectedNodeId, expandedNodeIds: [...expandedNodeIds], ...(narrow && !detailsMode ? { treeOnly: true } : {}), ...(narrow && detailsMode ? { detailsOnly: true } : {}), ...(actions ? { actions } : {}) }, styles);
|
|
3224
|
+
const statusLines = stopStatus ? truncateToVisualLines(styles.error(stopStatus), Number.MAX_SAFE_INTEGER, width, 0).visualLines.map((line) => line.trimEnd()) : [];
|
|
3225
|
+
const content = [...statusLines, ...phaseLines];
|
|
3226
|
+
const rows = terminalRows();
|
|
3227
|
+
const hintRows = rows >= 3 ? 1 : 0;
|
|
3228
|
+
const viewport = Math.max(1, rows - hintRows);
|
|
3229
|
+
const maxOffset = Math.max(0, content.length - viewport);
|
|
3230
|
+
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
3231
|
+
if (actionMode) {
|
|
3232
|
+
const label = actions?.options[actionIndex];
|
|
3233
|
+
const actionRow = label ? content.findIndex((line) => line.includes(label)) : -1;
|
|
3234
|
+
if (actionRow >= 0) {
|
|
3235
|
+
if (actionRow < dashboardOffset)
|
|
3236
|
+
dashboardOffset = actionRow;
|
|
3237
|
+
else if (actionRow >= dashboardOffset + viewport)
|
|
3238
|
+
dashboardOffset = actionRow - viewport + 1;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
else if (!detailsMode && selectionNeedsScroll) {
|
|
3242
|
+
const selectedRow = content.findIndex((line) => line.startsWith("→"));
|
|
3243
|
+
if (selectedRow >= 0) {
|
|
3244
|
+
if (selectedRow < dashboardOffset)
|
|
3245
|
+
dashboardOffset = selectedRow;
|
|
3246
|
+
else if (selectedRow >= dashboardOffset + viewport)
|
|
3247
|
+
dashboardOffset = selectedRow - viewport + 1;
|
|
3248
|
+
}
|
|
3249
|
+
selectionNeedsScroll = false;
|
|
2850
3250
|
}
|
|
2851
|
-
const maxOffset = Math.max(0, dashboardLines.length - layout.dashboardViewport);
|
|
2852
3251
|
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
|
-
];
|
|
3252
|
+
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" : "close"}${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] ?? "";
|
|
3253
|
+
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
2860
3254
|
},
|
|
2861
3255
|
invalidate() { },
|
|
2862
3256
|
handleInput(data) {
|
|
2863
|
-
if (stopRequested)
|
|
3257
|
+
if (stopRequested || editorRunning)
|
|
3258
|
+
return;
|
|
3259
|
+
const narrow = renderedWidth < 80;
|
|
3260
|
+
if (!actionMode && (data === "a" || data === "A")) {
|
|
3261
|
+
actionMode = true;
|
|
3262
|
+
actionIndex = 0;
|
|
3263
|
+
dashboardOffset = 0;
|
|
3264
|
+
tui.requestRender();
|
|
2864
3265
|
return;
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3266
|
+
}
|
|
3267
|
+
if (actionMode) {
|
|
3268
|
+
const options = actionOptions();
|
|
3269
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
3270
|
+
actionMode = false;
|
|
3271
|
+
dashboardOffset = 0;
|
|
3272
|
+
tui.requestRender();
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (keybindings.matches(data, "tui.select.up"))
|
|
3276
|
+
actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
3277
|
+
else if (keybindings.matches(data, "tui.select.down"))
|
|
3278
|
+
actionIndex = (actionIndex + 1) % options.length;
|
|
3279
|
+
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3280
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3281
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3282
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3283
|
+
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
3284
|
+
const action = options[actionIndex];
|
|
3285
|
+
const agent = selectedAgentRecord();
|
|
3286
|
+
if (!action || action === "Back") {
|
|
3287
|
+
actionMode = false;
|
|
3288
|
+
dashboardOffset = 0;
|
|
3289
|
+
}
|
|
3290
|
+
else if (agent) {
|
|
3291
|
+
const worktree = agentWorktreeFor(view, agent);
|
|
3292
|
+
if (action === "Open result in editor") {
|
|
3293
|
+
const result = view.agentResults.get(agent.id);
|
|
3294
|
+
if (result !== undefined)
|
|
3295
|
+
void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
|
|
3296
|
+
}
|
|
3297
|
+
else if (action === "Copy agent ID")
|
|
3298
|
+
void copyArtifact(agent.id, "agent ID");
|
|
3299
|
+
else if (action === "Copy branch" && worktree)
|
|
3300
|
+
void copyArtifact(worktree.branch, "branch");
|
|
3301
|
+
else if (action === "Copy worktree path" && worktree)
|
|
3302
|
+
void copyArtifact(worktree.path, "worktree path");
|
|
3303
|
+
else
|
|
3304
|
+
done(`__workflow_fork__:${agent.id}`);
|
|
3305
|
+
}
|
|
3306
|
+
else if (action === "Open script in editor")
|
|
3307
|
+
void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
|
|
3308
|
+
else if (action === "Stop")
|
|
3309
|
+
requestStop();
|
|
3310
|
+
else
|
|
3311
|
+
done(action);
|
|
2876
3312
|
}
|
|
3313
|
+
tui.requestRender();
|
|
3314
|
+
return;
|
|
2877
3315
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
if (
|
|
2881
|
-
|
|
2882
|
-
|
|
3316
|
+
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3317
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
3318
|
+
if (narrow && detailsMode) {
|
|
3319
|
+
detailsMode = false;
|
|
3320
|
+
selectionNeedsScroll = true;
|
|
2883
3321
|
}
|
|
3322
|
+
else
|
|
3323
|
+
done(undefined);
|
|
2884
3324
|
}
|
|
2885
|
-
else if (
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3325
|
+
else if (narrow && detailsMode) {
|
|
3326
|
+
if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3327
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3328
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3329
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3330
|
+
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
3331
|
+
if (current?.kind === "agent" && current.agentId) {
|
|
3332
|
+
actionMode = true;
|
|
3333
|
+
actionIndex = 0;
|
|
3334
|
+
}
|
|
3335
|
+
else if (current?.children.length) {
|
|
3336
|
+
if (expandedNodeIds.has(current.id))
|
|
3337
|
+
expandedNodeIds.delete(current.id);
|
|
3338
|
+
else
|
|
3339
|
+
expandedNodeIds.add(current.id);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
else if (keybindings.matches(data, "tui.editor.cursorLeft")) {
|
|
3344
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
3345
|
+
selectedNodeId = next.nodeId;
|
|
3346
|
+
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3347
|
+
selectionNeedsScroll = true;
|
|
3348
|
+
}
|
|
3349
|
+
else if (keybindings.matches(data, "tui.editor.cursorRight")) {
|
|
3350
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
3351
|
+
selectedNodeId = next.nodeId;
|
|
3352
|
+
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3353
|
+
selectionNeedsScroll = true;
|
|
2891
3354
|
}
|
|
2892
|
-
else if (keybindings.matches(data, "tui.select.
|
|
2893
|
-
|
|
3355
|
+
else if (keybindings.matches(data, "tui.select.up")) {
|
|
3356
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
3357
|
+
selectedNodeId = next.nodeId;
|
|
3358
|
+
selectionNeedsScroll = true;
|
|
2894
3359
|
}
|
|
3360
|
+
else if (keybindings.matches(data, "tui.select.down")) {
|
|
3361
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
3362
|
+
selectedNodeId = next.nodeId;
|
|
3363
|
+
selectionNeedsScroll = true;
|
|
3364
|
+
}
|
|
3365
|
+
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3366
|
+
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3367
|
+
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3368
|
+
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
2895
3369
|
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2898
|
-
else
|
|
2899
|
-
|
|
3370
|
+
if (narrow)
|
|
3371
|
+
detailsMode = true;
|
|
3372
|
+
else if (current?.kind === "agent" && current.agentId) {
|
|
3373
|
+
actionMode = true;
|
|
3374
|
+
actionIndex = 0;
|
|
3375
|
+
}
|
|
3376
|
+
else if (current?.children.length) {
|
|
3377
|
+
if (expandedNodeIds.has(current.id))
|
|
3378
|
+
expandedNodeIds.delete(current.id);
|
|
3379
|
+
else
|
|
3380
|
+
expandedNodeIds.add(current.id);
|
|
3381
|
+
}
|
|
2900
3382
|
}
|
|
2901
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
2902
|
-
done(undefined);
|
|
2903
3383
|
tui.requestRender();
|
|
2904
3384
|
},
|
|
2905
3385
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
2906
3386
|
}, theme);
|
|
2907
|
-
}, { overlay: true })
|
|
3387
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS })
|
|
2908
3388
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
2909
3389
|
if (!actionChoice || actionChoice === "Close")
|
|
2910
3390
|
return;
|
|
@@ -2912,48 +3392,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2912
3392
|
await selectAgent(view);
|
|
2913
3393
|
continue;
|
|
2914
3394
|
}
|
|
2915
|
-
if (actionChoice
|
|
3395
|
+
if (actionChoice.startsWith("__workflow_agent__:")) {
|
|
3396
|
+
await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
|
|
2916
3397
|
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%" } });
|
|
3398
|
+
}
|
|
3399
|
+
if (actionChoice.startsWith("__workflow_fork__:")) {
|
|
3400
|
+
const agentId = actionChoice.slice("__workflow_fork__:".length);
|
|
3401
|
+
const agent = view.agents.find((candidate) => candidate.id === agentId);
|
|
3402
|
+
if (agent)
|
|
3403
|
+
await forkAgentSession(view, agent);
|
|
2955
3404
|
continue;
|
|
2956
3405
|
}
|
|
3406
|
+
if (actionChoice === "Refresh")
|
|
3407
|
+
continue;
|
|
2957
3408
|
const copy = view.copies.get(actionChoice);
|
|
2958
3409
|
if (copy) {
|
|
2959
3410
|
await copyArtifact(copy.value, copy.artifact);
|
|
@@ -3017,7 +3468,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3017
3468
|
tui.requestRender();
|
|
3018
3469
|
},
|
|
3019
3470
|
}, theme);
|
|
3020
|
-
}, { overlay: true, overlayOptions:
|
|
3471
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS });
|
|
3021
3472
|
if (decision) {
|
|
3022
3473
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3023
3474
|
if (!accepted)
|
|
@@ -3044,16 +3495,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3044
3495
|
pi.on("session_shutdown", async () => {
|
|
3045
3496
|
try {
|
|
3046
3497
|
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)) {
|
|
3498
|
+
const isTerminal = SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state);
|
|
3499
|
+
if (!isTerminal) {
|
|
3052
3500
|
try {
|
|
3053
3501
|
await run.lifecycle.terminal("interrupted");
|
|
3054
3502
|
}
|
|
3055
3503
|
catch (error) {
|
|
3056
|
-
if (!
|
|
3504
|
+
if (!SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
3057
3505
|
throw error;
|
|
3058
3506
|
}
|
|
3059
3507
|
run.abortController.abort();
|