pi-extensible-workflows 3.0.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 +15 -17
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +83 -4
- package/dist/src/host.js +1246 -410
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +135 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +157 -31
- 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 +5 -3
- package/skills/pi-extensible-workflows/SKILL.md +45 -18
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +13 -4
- package/src/host.ts +1039 -366
- package/src/index.ts +5 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +53 -8
- package/src/utils.ts +39 -5
- package/src/validation.ts +130 -31
- package/src/workflow-artifacts.ts +34 -0
package/dist/src/host.js
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
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";
|
|
12
13
|
import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
13
|
-
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
14
|
-
import { launchScriptForSnapshot, loadAgentDefinitions,
|
|
14
|
+
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelAliasErrorName, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
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"]);
|
|
24
|
+
function snapshotResourcePolicy(snapshot, cwd, projectTrusted, globalSettingsPath) {
|
|
25
|
+
const empty = { skills: [], extensions: [] };
|
|
26
|
+
return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: snapshot.settings.disabledAgentResources ?? empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
27
|
+
}
|
|
19
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 };
|
|
29
|
+
function workflowLaunchSettings(cwd, projectTrusted, globalSettingsPath, concurrency) {
|
|
30
|
+
const resolution = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
31
|
+
const settings = Object.freeze({ ...resolution.effective, ...(concurrency === undefined ? {} : { concurrency }) });
|
|
32
|
+
return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath) };
|
|
33
|
+
}
|
|
34
|
+
function frozenResourcePolicy(policy) { return () => structuredClone(policy); }
|
|
35
|
+
function resumedSnapshotSettings(snapshot, resolution, modelAliases) {
|
|
36
|
+
const settings = { ...snapshot.settings, concurrency: snapshot.settingsSources === undefined || snapshot.settingsSources.concurrency === "per-run options" ? snapshot.settings.concurrency : resolution.effective.concurrency, modelAliases };
|
|
37
|
+
if (resolution.effective.disabledAgentResources === undefined)
|
|
38
|
+
delete settings.disabledAgentResources;
|
|
39
|
+
else
|
|
40
|
+
settings.disabledAgentResources = resolution.effective.disabledAgentResources;
|
|
41
|
+
const settingsSources = snapshot.settingsSources === undefined ? undefined : { ...snapshot.settingsSources, modelAliases: resolution.sources.modelAliases, disabledAgentResources: resolution.sources.disabledAgentResources, concurrency: snapshot.settingsSources.concurrency === "per-run options" ? "per-run options" : resolution.sources.concurrency };
|
|
42
|
+
return { settings, ...(settingsSources === undefined ? {} : { settingsSources }) };
|
|
43
|
+
}
|
|
20
44
|
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
21
45
|
function workflowDetail(message) {
|
|
22
46
|
const detail = message.trim().replace(new RegExp(`\\b(?:${ERROR_CODES.join("|")})\\b:?\\s*`, "g"), "").replace(/^\s*[A-Z][A-Z0-9_]+:\s*/, "").split("\n").filter((line) => !/^\s*at\s/.test(line)).join("\n").replace(/^Run \S+(?=\s(?:exceeded|is))/i, "Run").replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "the workflow").replace(/^(?:Pi )session \S+(?=\s(?:is|has))/i, "session").replace(/^(Unknown scheduler run|Missing production ownership record|Persisted agent belongs to another run):\s*\S+/i, "$1").replace(/\b(?:runId|sessionId|callSite|occurrence|failedAt|id)[:=]\s*\S+/gi, "").replace(/\s{2,}/g, " ").trim();
|
|
@@ -62,7 +86,6 @@ function mainAgentError(error) {
|
|
|
62
86
|
const typed = asWorkflowError(error);
|
|
63
87
|
const presented = new WorkflowError(typed.code, formatWorkflowFailure(typed));
|
|
64
88
|
Object.assign(presented, typed);
|
|
65
|
-
presented.message = formatWorkflowFailure(typed);
|
|
66
89
|
return presented;
|
|
67
90
|
}
|
|
68
91
|
export class RunLifecycle {
|
|
@@ -125,7 +148,7 @@ export class RunLifecycle {
|
|
|
125
148
|
await this.enter();
|
|
126
149
|
}
|
|
127
150
|
async terminal(state, reason) {
|
|
128
|
-
if (
|
|
151
|
+
if (HARD_TERMINAL_RUN_STATES.has(this.#state))
|
|
129
152
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `${this.#state} runs are terminal`);
|
|
130
153
|
await this.#set(state, reason ?? state);
|
|
131
154
|
for (const resolve of this.#waiters.splice(0))
|
|
@@ -146,20 +169,337 @@ export function formatWorkflowPreview(args) {
|
|
|
146
169
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
147
170
|
}
|
|
148
171
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
149
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
150
|
-
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
|
+
}
|
|
151
196
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
152
|
-
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" })),
|
|
153
198
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
154
|
-
script: Type.Optional(Type.String({ description: "Immutable workflow source
|
|
155
|
-
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" })),
|
|
156
201
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
157
|
-
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of
|
|
158
|
-
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
159
|
-
budget: Type.Optional(Type.Unknown({ description: "
|
|
160
|
-
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" })),
|
|
161
206
|
});
|
|
162
207
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
208
|
+
function phaseNames(source) {
|
|
209
|
+
const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
|
|
210
|
+
return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
211
|
+
}
|
|
212
|
+
function phaseAgentCounts(agents) {
|
|
213
|
+
const counts = { total: agents.length, completed: 0, running: 0, failed: 0, cancelled: 0, pending: 0 };
|
|
214
|
+
for (const agent of agents) {
|
|
215
|
+
if (agent.state === "completed")
|
|
216
|
+
counts.completed += 1;
|
|
217
|
+
else if (agent.state === "running")
|
|
218
|
+
counts.running += 1;
|
|
219
|
+
else if (agent.state === "failed")
|
|
220
|
+
counts.failed += 1;
|
|
221
|
+
else if (agent.state === "cancelled")
|
|
222
|
+
counts.cancelled += 1;
|
|
223
|
+
else
|
|
224
|
+
counts.pending += 1;
|
|
225
|
+
}
|
|
226
|
+
return counts;
|
|
227
|
+
}
|
|
228
|
+
function phaseState(runState, counts, isLatest) {
|
|
229
|
+
if (!isLatest)
|
|
230
|
+
return "completed";
|
|
231
|
+
if (runState === "failed")
|
|
232
|
+
return "failed";
|
|
233
|
+
if (runState === "stopped")
|
|
234
|
+
return "cancelled";
|
|
235
|
+
if (runState === "interrupted")
|
|
236
|
+
return "interrupted";
|
|
237
|
+
if (runState === "budget_exhausted")
|
|
238
|
+
return "budget_exhausted";
|
|
239
|
+
if (counts.failed > 0)
|
|
240
|
+
return "failed";
|
|
241
|
+
if (counts.cancelled > 0)
|
|
242
|
+
return "cancelled";
|
|
243
|
+
if (counts.running > 0 || counts.pending > 0)
|
|
244
|
+
return "running";
|
|
245
|
+
return runState === "completed" ? "completed" : "running";
|
|
246
|
+
}
|
|
247
|
+
export function buildWorkflowPhaseModel(run, source) {
|
|
248
|
+
const declaredPhases = phaseNames(source);
|
|
249
|
+
const rawHistory = Array.isArray(run.phaseHistory) ? run.phaseHistory : [];
|
|
250
|
+
const observed = [];
|
|
251
|
+
let boundary = 0;
|
|
252
|
+
for (const record of rawHistory) {
|
|
253
|
+
if (!object(record) || typeof record.phase !== "string" || !record.phase.trim() || typeof record.afterAgent !== "number" || !Number.isSafeInteger(record.afterAgent))
|
|
254
|
+
continue;
|
|
255
|
+
boundary = Math.max(boundary, Math.min(run.agents.length, Math.max(0, record.afterAgent)));
|
|
256
|
+
observed.push({ name: record.phase.trim(), afterAgent: boundary });
|
|
257
|
+
}
|
|
258
|
+
if (!observed.length && typeof run.phase === "string" && run.phase.trim())
|
|
259
|
+
observed.push({ name: run.phase.trim(), afterAgent: 0 });
|
|
260
|
+
const observedEntries = observed.map((entry, index) => ({ ...entry, index, agents: run.agents.slice(entry.afterAgent, observed[index + 1]?.afterAgent ?? run.agents.length) }));
|
|
261
|
+
const matchedDeclarations = new Set();
|
|
262
|
+
const declarationIndices = observedEntries.map((entry) => {
|
|
263
|
+
const index = declaredPhases.findIndex((name, candidate) => !matchedDeclarations.has(candidate) && name === entry.name);
|
|
264
|
+
if (index >= 0)
|
|
265
|
+
matchedDeclarations.add(index);
|
|
266
|
+
return index >= 0 ? index : undefined;
|
|
267
|
+
});
|
|
268
|
+
const entries = observedEntries.map((entry, index) => ({ name: entry.name, observedIndex: index, ...(declarationIndices[index] === undefined ? {} : { declarationIndex: declarationIndices[index] }) }));
|
|
269
|
+
for (const [declarationIndex, name] of declaredPhases.entries()) {
|
|
270
|
+
if (matchedDeclarations.has(declarationIndex))
|
|
271
|
+
continue;
|
|
272
|
+
const insertion = entries.findIndex((entry) => entry.declarationIndex !== undefined && entry.declarationIndex > declarationIndex);
|
|
273
|
+
const pending = { name };
|
|
274
|
+
if (insertion < 0)
|
|
275
|
+
entries.push(pending);
|
|
276
|
+
else
|
|
277
|
+
entries.splice(insertion, 0, pending);
|
|
278
|
+
}
|
|
279
|
+
const occurrences = new Map();
|
|
280
|
+
const phases = entries.map((entry) => {
|
|
281
|
+
const occurrence = (occurrences.get(entry.name) ?? 0) + 1;
|
|
282
|
+
occurrences.set(entry.name, occurrence);
|
|
283
|
+
const observation = entry.observedIndex === undefined ? undefined : observedEntries[entry.observedIndex];
|
|
284
|
+
const agents = observation?.agents ?? [];
|
|
285
|
+
const counts = phaseAgentCounts(agents);
|
|
286
|
+
const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1) : "not started";
|
|
287
|
+
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
|
|
288
|
+
});
|
|
289
|
+
let currentPhaseIndex;
|
|
290
|
+
for (let index = phases.length - 1; index >= 0; index -= 1) {
|
|
291
|
+
if (phases[index]?.observed) {
|
|
292
|
+
currentPhaseIndex = index;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const counts = {};
|
|
297
|
+
for (const phase of phases)
|
|
298
|
+
counts[phase.state] = (counts[phase.state] ?? 0) + 1;
|
|
299
|
+
const current = currentPhaseIndex === undefined ? undefined : phases[currentPhaseIndex];
|
|
300
|
+
const assigned = new Set(observedEntries.flatMap(({ agents }) => agents.map((agent) => agent.id)));
|
|
301
|
+
const unassignedAgents = run.agents.filter((agent) => !assigned.has(agent.id));
|
|
302
|
+
const result = { declaredPhases, phases, counts };
|
|
303
|
+
if (current !== undefined && currentPhaseIndex !== undefined) {
|
|
304
|
+
result.currentPhaseIndex = currentPhaseIndex;
|
|
305
|
+
result.currentPhaseId = current.id;
|
|
306
|
+
}
|
|
307
|
+
if (unassignedAgents.length)
|
|
308
|
+
result.unassignedAgents = unassignedAgents;
|
|
309
|
+
return result;
|
|
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
|
+
}
|
|
492
|
+
export function preserveWorkflowPhaseSelection(model, selection) {
|
|
493
|
+
const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
|
|
494
|
+
if (!phase)
|
|
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 } : {}) };
|
|
502
|
+
}
|
|
163
503
|
function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
164
504
|
function agentGroupLabel(agents) {
|
|
165
505
|
const structural = agents[0]?.structuralPath ?? [];
|
|
@@ -171,8 +511,13 @@ function agentGroups(agents, allAgents = agents) {
|
|
|
171
511
|
const groups = new Map();
|
|
172
512
|
for (const [index, agent] of agents.entries()) {
|
|
173
513
|
let depth = 0;
|
|
174
|
-
|
|
514
|
+
const seen = new Set([agent.id]);
|
|
515
|
+
for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) {
|
|
516
|
+
if (seen.has(parent))
|
|
517
|
+
break;
|
|
518
|
+
seen.add(parent);
|
|
175
519
|
depth += 1;
|
|
520
|
+
}
|
|
176
521
|
const key = agentGroupKey(agent);
|
|
177
522
|
const group = groups.get(key) ?? { agents: [] };
|
|
178
523
|
group.agents.push({ agent, index, depth });
|
|
@@ -188,26 +533,31 @@ function renderGroupedAgents(agents, render, allAgents = agents, groupLabel = (l
|
|
|
188
533
|
...group.entries.map((entry) => render(entry, grouped)),
|
|
189
534
|
]);
|
|
190
535
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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);
|
|
199
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); }
|
|
200
550
|
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
201
551
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
202
|
-
const workflowIcon = run.state
|
|
203
|
-
const
|
|
552
|
+
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
553
|
+
const iconStyle = workflowIconStyle(run.state, styles);
|
|
204
554
|
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
205
|
-
const lines = [`${
|
|
555
|
+
const lines = [`${iconStyle(workflowIcon)} ${header}`];
|
|
206
556
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
207
557
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
208
558
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
209
559
|
const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
210
|
-
const icon = agent.state
|
|
560
|
+
const icon = agentStateGlyph(agent.state, spinner);
|
|
211
561
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
212
562
|
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
213
563
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
@@ -239,7 +589,200 @@ function textBlock(text) {
|
|
|
239
589
|
invalidate() { },
|
|
240
590
|
};
|
|
241
591
|
}
|
|
242
|
-
|
|
592
|
+
function styledTextBlock(text) {
|
|
593
|
+
return {
|
|
594
|
+
render(width) {
|
|
595
|
+
return truncateWorkflowProgress(text, width);
|
|
596
|
+
},
|
|
597
|
+
invalidate() { },
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function workflowCatalogBlock(text, expanded) {
|
|
601
|
+
return {
|
|
602
|
+
render(width) {
|
|
603
|
+
const safeWidth = Math.max(1, width);
|
|
604
|
+
if (!expanded)
|
|
605
|
+
return truncateWorkflowProgress(text, safeWidth);
|
|
606
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines.map((line) => line.trimEnd());
|
|
607
|
+
},
|
|
608
|
+
invalidate() { },
|
|
609
|
+
};
|
|
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
|
+
}
|
|
708
|
+
function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
|
|
709
|
+
function catalogResultValue(result) {
|
|
710
|
+
if (result.details !== undefined)
|
|
711
|
+
return result.details;
|
|
712
|
+
const text = result.content?.find((entry) => entry.type === "text")?.text;
|
|
713
|
+
if (!text)
|
|
714
|
+
return undefined;
|
|
715
|
+
try {
|
|
716
|
+
return JSON.parse(text);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return text;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function isCatalogIndex(value) {
|
|
723
|
+
return object(value) && Array.isArray(value.functions) && Array.isArray(value.variables);
|
|
724
|
+
}
|
|
725
|
+
function isCatalogFunction(value) {
|
|
726
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.input) && object(value.output);
|
|
727
|
+
}
|
|
728
|
+
function isCatalogVariable(value) {
|
|
729
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.schema);
|
|
730
|
+
}
|
|
731
|
+
function isCatalogError(value) {
|
|
732
|
+
return object(value) && object(value.error) && typeof value.error.message === "string";
|
|
733
|
+
}
|
|
734
|
+
function catalogSectionTitle(label, count, theme) {
|
|
735
|
+
return theme.fg("accent", theme.bold(`${label} (${String(count)})`));
|
|
736
|
+
}
|
|
737
|
+
function catalogIndexEntries(entries, theme) {
|
|
738
|
+
const width = Math.max(0, ...entries.map((entry) => entry.name.length));
|
|
739
|
+
return entries.map((entry) => ` ${theme.fg("accent", entry.name.padEnd(width))} ${theme.fg("toolOutput", catalogText(entry.description))}`);
|
|
740
|
+
}
|
|
741
|
+
function formatCatalogIndex(catalog, theme) {
|
|
742
|
+
const aliases = Object.prototype.propertyIsEnumerable.call(catalog, "modelAliases") ? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" })) : catalog.modelAliasEntries ?? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" }));
|
|
743
|
+
const aliasWidth = Math.max(0, ...aliases.map(({ name }) => name.length));
|
|
744
|
+
const aliasLines = aliases.map(({ name, kind, provenance }) => ` ${theme.fg("accent", name.padEnd(aliasWidth))} ${theme.fg("toolOutput", `${kind} · ${provenance}`)}`);
|
|
745
|
+
return [
|
|
746
|
+
catalogSectionTitle("Functions", catalog.functions.length, theme),
|
|
747
|
+
...catalogIndexEntries(catalog.functions, theme),
|
|
748
|
+
"",
|
|
749
|
+
catalogSectionTitle("Variables", catalog.variables.length, theme),
|
|
750
|
+
...catalogIndexEntries(catalog.variables, theme),
|
|
751
|
+
"",
|
|
752
|
+
catalogSectionTitle("Model aliases", aliases.length, theme),
|
|
753
|
+
...aliasLines,
|
|
754
|
+
].join("\n");
|
|
755
|
+
}
|
|
756
|
+
function catalogSchemaLines(schema, theme) {
|
|
757
|
+
const json = JSON.stringify(schema, null, 2);
|
|
758
|
+
return json.split("\n").map((line) => ` ${theme.fg("toolOutput", line)}`);
|
|
759
|
+
}
|
|
760
|
+
function formatCatalogDetail(value, expanded, theme) {
|
|
761
|
+
if ("kind" in value)
|
|
762
|
+
return [theme.fg("accent", theme.bold("Model alias")), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", `${value.kind} · ${value.provenance}`)}`].join("\n");
|
|
763
|
+
const kind = "input" in value ? "Function" : "Variable";
|
|
764
|
+
if (!expanded)
|
|
765
|
+
return [theme.fg("accent", theme.bold(kind)), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", catalogText(value.description))}`, ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)} ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", catalogText(value.headline))}`].join("\n");
|
|
766
|
+
const lines = [theme.fg("accent", theme.bold(`${kind}: ${value.name}`)), `${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.description)}`, "", theme.fg("accent", theme.bold("Extension")), ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)}`, ` ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", value.headline)}`, ` ${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.extensionDescription)}`, "", theme.fg("accent", theme.bold("Schema"))];
|
|
767
|
+
if ("input" in value)
|
|
768
|
+
lines.push(theme.fg("muted", "Input schema"), ...catalogSchemaLines(value.input, theme), "", theme.fg("muted", "Output schema"), ...catalogSchemaLines(value.output, theme));
|
|
769
|
+
else
|
|
770
|
+
lines.push(theme.fg("muted", "Variable schema"), ...catalogSchemaLines(value.schema, theme));
|
|
771
|
+
return lines.join("\n");
|
|
772
|
+
}
|
|
773
|
+
function formatWorkflowCatalog(value, expanded, theme) {
|
|
774
|
+
if (isCatalogIndex(value))
|
|
775
|
+
return formatCatalogIndex(value, theme);
|
|
776
|
+
if (isCatalogFunction(value) || isCatalogVariable(value))
|
|
777
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
778
|
+
if (object(value) && typeof value.name === "string" && (value.kind === "static" || value.kind === "dynamic"))
|
|
779
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
780
|
+
if (isCatalogError(value))
|
|
781
|
+
return theme.fg("error", value.error.message);
|
|
782
|
+
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
783
|
+
}
|
|
784
|
+
const ANSI_SGR_SOURCE = `${String.fromCharCode(27)}\\[[0-9;]*m`;
|
|
785
|
+
const ANSI_SGR = new RegExp(ANSI_SGR_SOURCE);
|
|
243
786
|
export function truncateWorkflowProgress(text, width) {
|
|
244
787
|
const safeWidth = Math.max(1, width);
|
|
245
788
|
return text.split("\n").flatMap((line) => {
|
|
@@ -263,7 +806,7 @@ function themeWorkflowProgressStyles(theme) {
|
|
|
263
806
|
warning: (text) => theme.fg("warning", text),
|
|
264
807
|
muted: (text) => theme.fg("muted", text),
|
|
265
808
|
dim: (text) => theme.fg("dim", text),
|
|
266
|
-
bold: (text) => theme.bold(text),
|
|
809
|
+
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
267
810
|
};
|
|
268
811
|
}
|
|
269
812
|
function workflowProgressBlock(run, theme) {
|
|
@@ -310,39 +853,40 @@ function navigatorRunLabels(entries) {
|
|
|
310
853
|
nameCount.set(run.workflowName, (nameCount.get(run.workflowName) ?? 0) + 1);
|
|
311
854
|
return entries.map(({ store, loaded: { run } }) => {
|
|
312
855
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
313
|
-
const glyph = run.state
|
|
856
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
314
857
|
const suffix = (nameCount.get(run.workflowName) ?? 0) > 1 ? ` ${store.runId.slice(0, 8)}` : "";
|
|
315
858
|
const cost = run.agents.reduce((sum, a) => sum + (a.accounting?.cost ?? 0), 0);
|
|
316
859
|
const costStr = cost > 0 ? ` $${cost.toFixed(2)}` : "";
|
|
317
860
|
return `${glyph} ${run.workflowName}${suffix} ${run.state} ${run.phase ?? ""} ${String(done)}/${String(run.agents.length)} agents${costStr}`;
|
|
318
861
|
});
|
|
319
862
|
}
|
|
320
|
-
function agentBreadcrumbParts(agent, byId) {
|
|
321
|
-
const
|
|
322
|
-
const parts = agent.
|
|
863
|
+
export function agentBreadcrumbParts(agent, byId, includeStructuralPath = false) {
|
|
864
|
+
const leaf = agent.label ?? agent.name;
|
|
865
|
+
const parts = includeStructuralPath && agent.structuralPath?.length ? [agent.structuralPath.join(" > ")] : [];
|
|
866
|
+
if (agent.parentBreadcrumb)
|
|
867
|
+
parts.push(agent.parentBreadcrumb);
|
|
868
|
+
const ancestors = [];
|
|
323
869
|
const seen = new Set([agent.id]);
|
|
324
870
|
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
325
871
|
if (seen.has(parentId))
|
|
326
|
-
break;
|
|
872
|
+
break;
|
|
327
873
|
seen.add(parentId);
|
|
328
874
|
const parent = byId.get(parentId);
|
|
329
|
-
if (parent)
|
|
330
|
-
parts.push(parent.label ?? parent.name);
|
|
331
|
-
else
|
|
875
|
+
if (!parent)
|
|
332
876
|
break;
|
|
877
|
+
ancestors.push(parent.label ?? parent.name);
|
|
333
878
|
}
|
|
334
|
-
parts.push(
|
|
879
|
+
parts.push(...ancestors.reverse(), leaf);
|
|
335
880
|
return parts;
|
|
336
881
|
}
|
|
337
|
-
function agentBreadcrumb(agent, byId) {
|
|
338
|
-
|
|
339
|
-
return parts.length > 1 ? parts.join(" > ") : parts[0] ?? "";
|
|
882
|
+
export function agentBreadcrumb(agent, byId, includeStructuralPath = false) {
|
|
883
|
+
return agentBreadcrumbParts(agent, byId, includeStructuralPath).join(" > ");
|
|
340
884
|
}
|
|
341
885
|
function styledAgentBreadcrumb(agent, byId, styles) {
|
|
342
886
|
const parts = agentBreadcrumbParts(agent, byId);
|
|
343
887
|
if (parts.length <= 1)
|
|
344
888
|
return parts[0] ?? "";
|
|
345
|
-
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${parts[parts.length - 1] ?? ""}`;
|
|
889
|
+
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
346
890
|
}
|
|
347
891
|
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
348
892
|
const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
|
|
@@ -357,7 +901,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
357
901
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
358
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 });
|
|
359
903
|
const hasAccounting = run.agents.some((a) => a.accounting);
|
|
360
|
-
const glyph = run.state
|
|
904
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
361
905
|
const header = `${glyph} ${run.workflowName}`;
|
|
362
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(" · ");
|
|
363
907
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
@@ -368,7 +912,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
368
912
|
lines.push("");
|
|
369
913
|
const byId = new Map(run.agents.map((a) => [a.id, a]));
|
|
370
914
|
const render = ({ agent, depth }, grouped) => {
|
|
371
|
-
const icon = agent.state
|
|
915
|
+
const icon = agentStateGlyph(agent.state, "⠦");
|
|
372
916
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
373
917
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
374
918
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
@@ -391,7 +935,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
391
935
|
}
|
|
392
936
|
return lines.join("\n");
|
|
393
937
|
}
|
|
394
|
-
export function formatNavigatorRun(loaded, checkpoints,
|
|
938
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
395
939
|
const { run, snapshot } = loaded;
|
|
396
940
|
const lines = [
|
|
397
941
|
`Workflow: ${run.workflowName}`,
|
|
@@ -401,6 +945,7 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
401
945
|
`Launch cwd: ${run.cwd}`,
|
|
402
946
|
...formatCompactBudgetStatus(run),
|
|
403
947
|
`Launch models: ${snapshot.models.join(", ") || "(none)"}`,
|
|
948
|
+
`Settings: concurrency=${String(snapshot.settings.concurrency)}`,
|
|
404
949
|
];
|
|
405
950
|
if (run.error)
|
|
406
951
|
lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
@@ -409,6 +954,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
409
954
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
410
955
|
if (aliases && Object.keys(aliases).length)
|
|
411
956
|
lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
|
|
957
|
+
if (snapshot.settingsSources)
|
|
958
|
+
lines.push(`Settings sources: concurrency=${snapshot.settingsSources.concurrency}, modelAliases=${snapshot.settingsSources.modelAliases}, disabledAgentResources=${snapshot.settingsSources.disabledAgentResources}`);
|
|
412
959
|
lines.push("Agents / ownership:");
|
|
413
960
|
if (!run.agents.length)
|
|
414
961
|
lines.push(" (none)");
|
|
@@ -431,10 +978,115 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
431
978
|
lines.push(" (none)");
|
|
432
979
|
for (const checkpoint of checkpoints)
|
|
433
980
|
lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
434
|
-
lines.push(`Worktrees: ${String(
|
|
981
|
+
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
435
982
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
436
983
|
return lines.join("\n");
|
|
437
984
|
}
|
|
985
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
986
|
+
const safeWidth = Math.max(1, width);
|
|
987
|
+
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
988
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
989
|
+
const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
|
|
990
|
+
const wrap = (text, limit = safeWidth) => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
|
|
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
|
+
};
|
|
1052
|
+
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
1053
|
+
const statusSummary = stateNames.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
|
|
1054
|
+
const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
1055
|
+
if (run.error)
|
|
1056
|
+
lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
1057
|
+
lines.push(`phase: ${run.phase ?? selectedPhase?.name ?? "none"}`, `Run state: ${run.state}`, `Phases: ${statusSummary}`);
|
|
1058
|
+
for (const event of run.events ?? [])
|
|
1059
|
+
lines.push(styles.warning(`Warning: ${event.message}`));
|
|
1060
|
+
lines.push(...formatCompactBudgetStatus(run));
|
|
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}`)];
|
|
1067
|
+
};
|
|
1068
|
+
const detailRows = () => [...details(selectedNode), ...actionRows()];
|
|
1069
|
+
if (safeWidth >= 80) {
|
|
1070
|
+
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
1071
|
+
const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
|
|
1072
|
+
const sidebar = renderTree(sidebarWidth).flatMap((line) => wrap(line, sidebarWidth));
|
|
1073
|
+
const detail = detailRows().flatMap((line) => wrap(line, detailWidth));
|
|
1074
|
+
const rows = Math.max(sidebar.length, detail.length);
|
|
1075
|
+
for (let index = 0; index < rows; index += 1)
|
|
1076
|
+
lines.push(`${padTo(sidebar[index] ?? "", sidebarWidth)} | ${detail[index] ?? ""}`);
|
|
1077
|
+
}
|
|
1078
|
+
else if (selection.detailsOnly) {
|
|
1079
|
+
lines.push(...detailRows().flatMap((line) => wrap(line)));
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
lines.push(...renderTree(safeWidth));
|
|
1083
|
+
if (!selection.treeOnly)
|
|
1084
|
+
lines.push("", ...detailRows().flatMap((line) => wrap(line)));
|
|
1085
|
+
}
|
|
1086
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
|
|
1087
|
+
lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
1088
|
+
return lines.flatMap((line) => wrap(line));
|
|
1089
|
+
}
|
|
438
1090
|
function formatCheckpointReview(checkpoint) {
|
|
439
1091
|
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
440
1092
|
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
@@ -920,42 +1572,70 @@ function projectTrusted(ctx) {
|
|
|
920
1572
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
921
1573
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
922
1574
|
}
|
|
923
|
-
function
|
|
1575
|
+
function asFn(value) { return typeof value === "function" ? value : undefined; }
|
|
924
1576
|
function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
|
|
925
1577
|
function piHostCapabilities(pi) {
|
|
926
1578
|
if (!object(pi))
|
|
927
1579
|
return {};
|
|
928
|
-
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
1580
|
+
const registerEntryRenderer = asFn(pi.registerEntryRenderer);
|
|
929
1581
|
const events = pi.events;
|
|
930
|
-
return { ...(
|
|
1582
|
+
return { ...(registerEntryRenderer ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
931
1583
|
}
|
|
932
|
-
function isModelRegistryGetter(value) { return typeof value === "function"; }
|
|
933
1584
|
function contextHostCapabilities(ctx) {
|
|
934
1585
|
if (!object(ctx) || !object(ctx.modelRegistry))
|
|
935
1586
|
return {};
|
|
936
1587
|
const registry = ctx.modelRegistry;
|
|
937
|
-
const getAll = registry.getAll;
|
|
938
|
-
const getAvailable = registry.getAvailable;
|
|
939
|
-
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) } : {}) } };
|
|
1591
|
+
}
|
|
1592
|
+
function modelInventory(root, registry) {
|
|
1593
|
+
const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
1594
|
+
const available = registry?.getAvailable?.() ?? registry?.getAll?.() ?? [];
|
|
1595
|
+
const knownModels = new Set(all.map((model) => `${model.provider}/${model.id}`));
|
|
1596
|
+
const availableModels = new Set(available.map((model) => `${model.provider}/${model.id}`));
|
|
1597
|
+
const rootName = root?.provider && root.model ? `${root.provider}/${root.model}` : undefined;
|
|
1598
|
+
if (rootName) {
|
|
1599
|
+
knownModels.add(rootName);
|
|
1600
|
+
availableModels.add(rootName);
|
|
1601
|
+
}
|
|
1602
|
+
return { knownModels, availableModels };
|
|
1603
|
+
}
|
|
1604
|
+
function resumeHostContext(ctx) {
|
|
1605
|
+
const model = object(ctx) && object(ctx.model) && typeof ctx.model.provider === "string" && typeof ctx.model.id === "string" ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
1606
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry };
|
|
1607
|
+
}
|
|
1608
|
+
async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
|
|
1609
|
+
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
1610
|
+
const dynamicNames = Object.keys(dynamic);
|
|
1611
|
+
try {
|
|
1612
|
+
const aliases = validateModelAliases({ ...dynamic, ...staticAliases }, settingsPath);
|
|
1613
|
+
validateModelAliasAvailability(aliases, dynamicNames, availableModels, knownModels, settingsPath);
|
|
1614
|
+
return { aliases, dynamicNames };
|
|
1615
|
+
}
|
|
1616
|
+
catch (error) {
|
|
1617
|
+
const name = modelAliasErrorName(error);
|
|
1618
|
+
const descriptor = name && typeof registry.modelAliases === "function" ? registry.modelAliases().find((candidate) => candidate.name === name) : undefined;
|
|
1619
|
+
if (descriptor && errorCode(error) !== "CANCELLED")
|
|
1620
|
+
throw new WorkflowError(errorCode(error) ?? "CONFIG_ERROR", `${errorText(error)} (extension: ${descriptor.headline})`);
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
940
1623
|
}
|
|
941
|
-
function isUiSelect(value) { return typeof value === "function"; }
|
|
942
|
-
function isUiInput(value) { return typeof value === "function"; }
|
|
943
|
-
function isUiSetStatus(value) { return typeof value === "function"; }
|
|
944
1624
|
function uiHostCapabilities(ui) {
|
|
945
1625
|
if (!object(ui))
|
|
946
1626
|
return undefined;
|
|
947
|
-
const select = ui.select;
|
|
948
|
-
const input = ui.input;
|
|
949
|
-
const setStatus = ui.setStatus;
|
|
950
|
-
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 } : {}) };
|
|
951
1631
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
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;
|
|
956
1635
|
}
|
|
957
|
-
function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
|
|
958
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 } };
|
|
959
1639
|
function borderWorkflowOverlay(component, theme) {
|
|
960
1640
|
return {
|
|
961
1641
|
...component,
|
|
@@ -965,13 +1645,10 @@ function borderWorkflowOverlay(component, theme) {
|
|
|
965
1645
|
},
|
|
966
1646
|
};
|
|
967
1647
|
}
|
|
968
|
-
function
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
return {};
|
|
972
|
-
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;
|
|
973
1651
|
}
|
|
974
|
-
function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
|
|
975
1652
|
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
|
|
976
1653
|
beginWorkflowExtensionLoading();
|
|
977
1654
|
const registry = loadingRegistry();
|
|
@@ -1084,11 +1761,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1084
1761
|
};
|
|
1085
1762
|
const phaseBridge = (store, metadata, lifecycle) => {
|
|
1086
1763
|
let cursor = 0;
|
|
1087
|
-
let executionPhase;
|
|
1088
1764
|
return async (phase) => {
|
|
1089
|
-
if (phase === executionPhase)
|
|
1090
|
-
return;
|
|
1091
|
-
executionPhase = phase;
|
|
1092
1765
|
await scheduler.flush();
|
|
1093
1766
|
await lifecycle.enter();
|
|
1094
1767
|
try {
|
|
@@ -1234,7 +1907,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1234
1907
|
catch {
|
|
1235
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 };
|
|
1236
1909
|
}
|
|
1237
|
-
|
|
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 } : {}) };
|
|
1238
1912
|
});
|
|
1239
1913
|
return { ...current, agents };
|
|
1240
1914
|
});
|
|
@@ -1243,7 +1917,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1243
1917
|
});
|
|
1244
1918
|
const cleanupTerminalRun = async (runId) => {
|
|
1245
1919
|
const run = runs.get(runId);
|
|
1246
|
-
if (!run || !
|
|
1920
|
+
if (!run || !HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
1247
1921
|
return;
|
|
1248
1922
|
await scheduler.cancelRun(runId);
|
|
1249
1923
|
await scheduler.flush();
|
|
@@ -1293,7 +1967,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1293
1967
|
run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
|
|
1294
1968
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1295
1969
|
};
|
|
1296
|
-
const answerBudgetDecision = async (runId, proposalId, approved, silent = false) => {
|
|
1970
|
+
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
|
|
1297
1971
|
const run = runs.get(runId);
|
|
1298
1972
|
if (!run)
|
|
1299
1973
|
return undefined;
|
|
@@ -1301,7 +1975,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1301
1975
|
if (!request)
|
|
1302
1976
|
return undefined;
|
|
1303
1977
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1304
|
-
const result = await applyBudgetDecision(request, approved);
|
|
1978
|
+
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
1305
1979
|
if (!silent)
|
|
1306
1980
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1307
1981
|
return result;
|
|
@@ -1362,10 +2036,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1362
2036
|
label: "Workflow Respond",
|
|
1363
2037
|
description: "Approve or reject one pending workflow checkpoint or budget decision",
|
|
1364
2038
|
parameters: Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
|
|
1365
|
-
async execute(_id, params) {
|
|
2039
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1366
2040
|
try {
|
|
1367
2041
|
if (params.proposalId) {
|
|
1368
|
-
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved);
|
|
2042
|
+
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
1369
2043
|
if (!result) {
|
|
1370
2044
|
const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
|
|
1371
2045
|
return { content: [{ type: "text", text: JSON.stringify(denied) }], details: denied };
|
|
@@ -1381,6 +2055,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1381
2055
|
throw mainAgentError(error);
|
|
1382
2056
|
}
|
|
1383
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); },
|
|
1384
2060
|
});
|
|
1385
2061
|
pi.registerTool({
|
|
1386
2062
|
name: "workflow_stop",
|
|
@@ -1396,49 +2072,111 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1396
2072
|
throw mainAgentError(error);
|
|
1397
2073
|
}
|
|
1398
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); },
|
|
1399
2077
|
});
|
|
1400
2078
|
let catalogRegistered = false;
|
|
1401
2079
|
let sessionStarted = false;
|
|
1402
|
-
const registerCatalog = () => {
|
|
2080
|
+
const registerCatalog = (cwd, trustedProject) => {
|
|
1403
2081
|
if (catalogRegistered || !pi.getActiveTools().includes("workflow"))
|
|
1404
2082
|
return;
|
|
1405
|
-
const catalog = registry.catalog();
|
|
1406
|
-
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
1407
|
-
|
|
2083
|
+
const catalog = registry.catalog({ cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) });
|
|
2084
|
+
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0 || Boolean(catalog.modelAliasEntries?.length);
|
|
2085
|
+
const hasSettings = catalog.settings !== undefined && [catalog.settings.globalSettingsPath, catalog.settings.projectSettingsPath].some((path) => existsSync(path));
|
|
2086
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases && !hasSettings)
|
|
1408
2087
|
return;
|
|
1409
2088
|
pi.registerTool({
|
|
1410
2089
|
name: "workflow_catalog",
|
|
1411
2090
|
label: "Workflow Catalog",
|
|
1412
2091
|
description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
|
|
1413
|
-
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or
|
|
2092
|
+
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
1414
2093
|
async execute(_id, params = {}) {
|
|
1415
|
-
const
|
|
2094
|
+
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
2095
|
+
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
1416
2096
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1417
|
-
}
|
|
2097
|
+
},
|
|
2098
|
+
renderCall(args, theme) {
|
|
2099
|
+
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
2100
|
+
return styledTextBlock(args.name === undefined ? title : `${title} ${theme.fg("accent", args.name)}`);
|
|
2101
|
+
},
|
|
2102
|
+
renderResult(result, options, theme) {
|
|
2103
|
+
return workflowCatalogBlock(formatWorkflowCatalog(catalogResultValue(result), options.expanded, theme), options.expanded);
|
|
2104
|
+
},
|
|
1418
2105
|
});
|
|
1419
2106
|
catalogRegistered = true;
|
|
1420
2107
|
};
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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));
|
|
1425
2115
|
if (missing)
|
|
1426
2116
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1427
2117
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1428
|
-
const
|
|
1429
|
-
resolveAgentResourcePolicy(
|
|
1430
|
-
const
|
|
1431
|
-
const previousAliases =
|
|
1432
|
-
const
|
|
1433
|
-
const knownModels =
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
2118
|
+
const resolution = resolveWorkflowSettings(input.cwd, input.trustedProject, settingsPath);
|
|
2119
|
+
const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
|
|
2120
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
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();
|
|
2175
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
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 });
|
|
1440
2177
|
await run.store.saveSnapshot(snapshot);
|
|
1441
|
-
|
|
2178
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
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) });
|
|
1442
2180
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
1443
2181
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1444
2182
|
if (drift.length)
|
|
@@ -1457,33 +2195,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1457
2195
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
1458
2196
|
if (missingRole)
|
|
1459
2197
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
|
|
1460
|
-
const
|
|
1461
|
-
const
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const previousAliases = loaded.snapshot.
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
if (context?.model)
|
|
1472
|
-
knownModels.add(`${context.model.provider}/${context.model.id}`);
|
|
1473
|
-
const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
|
|
1474
|
-
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1475
|
-
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1476
|
-
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1477
|
-
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1478
|
-
preflight(script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
1479
|
-
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2198
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
2199
|
+
const controller = new AbortController();
|
|
2200
|
+
if (context?.signal?.aborted)
|
|
2201
|
+
controller.abort();
|
|
2202
|
+
else {
|
|
2203
|
+
context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
|
|
2204
|
+
}
|
|
2205
|
+
run.abortController = controller;
|
|
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");
|
|
1480
2209
|
await run.store.saveSnapshot(snapshot);
|
|
1481
|
-
|
|
2210
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
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) });
|
|
1482
2212
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1483
2213
|
if (drift.length)
|
|
1484
2214
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1485
|
-
const controller = new AbortController();
|
|
1486
|
-
run.abortController = controller;
|
|
1487
2215
|
const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
|
|
1488
2216
|
run.executor.setRunContext(runContext);
|
|
1489
2217
|
let variables;
|
|
@@ -1492,7 +2220,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1492
2220
|
}
|
|
1493
2221
|
catch (error) {
|
|
1494
2222
|
const typed = asWorkflowError(error);
|
|
1495
|
-
if (!
|
|
2223
|
+
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
|
|
1496
2224
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
1497
2225
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
|
|
1498
2226
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
@@ -1505,37 +2233,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1505
2233
|
}
|
|
1506
2234
|
await scheduler.cancelRun(run.store.runId);
|
|
1507
2235
|
await run.lifecycle.resume();
|
|
1508
|
-
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 (
|
|
1509
|
-
await run.lifecycle.enter();
|
|
1510
|
-
try {
|
|
1511
|
-
const path = agentIdentityPath(identity);
|
|
1512
|
-
const replayed = await run.store.replay(path);
|
|
1513
|
-
if (replayed) {
|
|
1514
|
-
return replayed.value;
|
|
1515
|
-
}
|
|
1516
|
-
const worktree = agentWorktree(identity);
|
|
1517
|
-
const cwd = worktree.worktreeOwner ? (await persistWorktree(run.store, run.metadata, worktree.worktreeOwner)).cwd : run.store.cwd;
|
|
1518
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
1519
|
-
const model = typeof options.model === "string" ? options.model : undefined;
|
|
1520
|
-
const thinking = parseThinking(options.thinking);
|
|
1521
|
-
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1522
|
-
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 } : {}) });
|
|
1523
|
-
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
1524
|
-
const tools = resolved.tools;
|
|
1525
|
-
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
1526
|
-
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 });
|
|
1527
|
-
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
1528
|
-
signal.addEventListener("abort", cancel, { once: true });
|
|
1529
|
-
const outcome = await spawned.result.finally(() => { signal.removeEventListener("abort", cancel); });
|
|
1530
|
-
if (!outcome.ok)
|
|
1531
|
-
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
1532
|
-
await run.store.complete(path, outcome.value);
|
|
1533
|
-
return outcome.value;
|
|
1534
|
-
}
|
|
1535
|
-
finally {
|
|
1536
|
-
await run.lifecycle.leave();
|
|
1537
|
-
}
|
|
1538
|
-
}, 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);
|
|
1539
2237
|
run.execution = execution;
|
|
1540
2238
|
const completion = execution.result.then(async (value) => {
|
|
1541
2239
|
await scheduler.flush();
|
|
@@ -1562,7 +2260,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1562
2260
|
run.completion = completion;
|
|
1563
2261
|
void completion;
|
|
1564
2262
|
};
|
|
1565
|
-
const applyBudgetDecision = async (request, approved) => {
|
|
2263
|
+
const applyBudgetDecision = async (request, approved, context, signal) => {
|
|
1566
2264
|
const run = runs.get(request.runId);
|
|
1567
2265
|
if (!run)
|
|
1568
2266
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
@@ -1576,16 +2274,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1576
2274
|
next.budget = nextBudget;
|
|
1577
2275
|
else
|
|
1578
2276
|
delete next.budget; return next; });
|
|
1579
|
-
await coldResumeRun(run, false, {},
|
|
2277
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1580
2278
|
return { state: "running", approved: true };
|
|
1581
2279
|
};
|
|
1582
|
-
const resumeWorkflowRun = async (runId, rawPatch) => {
|
|
2280
|
+
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
1583
2281
|
const run = runs.get(runId);
|
|
1584
|
-
if (!run)
|
|
1585
|
-
|
|
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
|
+
}
|
|
1586
2299
|
const loaded = await run.store.load();
|
|
1587
2300
|
if (loaded.run.state !== "budget_exhausted")
|
|
1588
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "
|
|
2301
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", loaded.run.state));
|
|
1589
2302
|
const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1590
2303
|
const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
|
|
1591
2304
|
const nextBudget = mergeBudget(currentBudget, patch);
|
|
@@ -1610,11 +2323,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1610
2323
|
else
|
|
1611
2324
|
delete next.budget; return next; });
|
|
1612
2325
|
}
|
|
1613
|
-
await coldResumeRun(run, false, {},
|
|
2326
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1614
2327
|
return { state: "running" };
|
|
1615
2328
|
};
|
|
1616
2329
|
const retryReservations = new Set();
|
|
1617
|
-
const retryWorkflowRun = async (runId, context) => {
|
|
2330
|
+
const retryWorkflowRun = async (runId, context, signal) => {
|
|
1618
2331
|
if (typeof runId !== "string" || !runId.trim())
|
|
1619
2332
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
1620
2333
|
const host = object(context) ? context : {};
|
|
@@ -1630,10 +2343,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1630
2343
|
loaded = await sourceStore.load();
|
|
1631
2344
|
}
|
|
1632
2345
|
catch (error) {
|
|
1633
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `
|
|
2346
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session: ${errorText(error)}`);
|
|
1634
2347
|
}
|
|
1635
2348
|
if (loaded.run.state !== "failed")
|
|
1636
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE",
|
|
2349
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("retry", loaded.run.state));
|
|
1637
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")))
|
|
1638
2351
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "The source retry provenance is incomplete");
|
|
1639
2352
|
const lineageRootRunId = loaded.run.retry?.lineageRootRunId ?? loaded.run.id;
|
|
@@ -1669,24 +2382,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1669
2382
|
const missingRole = loaded.snapshot.agentTypes.find((role) => !loaded.snapshot.roles?.[role]);
|
|
1670
2383
|
if (missingRole)
|
|
1671
2384
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
|
|
1672
|
-
const active = new Set(pi.getActiveTools().filter((tool) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(tool)));
|
|
1673
|
-
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1674
|
-
if (missing)
|
|
1675
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1676
|
-
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1677
|
-
const currentSettings = loadSettings(settingsPath);
|
|
1678
|
-
resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
1679
|
-
const currentAliases = currentSettings.modelAliases ?? {};
|
|
1680
|
-
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1681
2385
|
const modelRegistry = contextHostCapabilities(context).modelRegistry;
|
|
1682
|
-
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
|
|
1683
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: "" };
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
|
|
1687
|
-
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1688
|
-
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1689
|
-
preflight(script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
2387
|
+
const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
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 });
|
|
1690
2389
|
await sourceStore.validateNamedWorktrees();
|
|
1691
2390
|
for (const name of loaded.run.retry?.namedWorktrees ?? [])
|
|
1692
2391
|
await sourceStore.resolveNamedWorktree(name);
|
|
@@ -1696,7 +2395,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1696
2395
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1697
2396
|
const childRunId = randomUUID();
|
|
1698
2397
|
const childStore = new RunStore(cwd, sessionId, childRunId, home);
|
|
1699
|
-
const childSnapshot =
|
|
2398
|
+
const childSnapshot = childBaseSnapshot;
|
|
1700
2399
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
1701
2400
|
const childInitialBudget = childBudget.snapshot();
|
|
1702
2401
|
const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
@@ -1705,13 +2404,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1705
2404
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
|
|
1706
2405
|
const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
|
|
1707
2406
|
const abortController = new AbortController();
|
|
1708
|
-
const providerErrorRecovery = createProviderErrorRecovery(context,
|
|
2407
|
+
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
1709
2408
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1710
|
-
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 } : {}) };
|
|
1711
2410
|
runs.set(childRunId, childRun);
|
|
1712
2411
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
1713
2412
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
1714
|
-
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry
|
|
2413
|
+
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
|
|
1715
2414
|
const completion = runs.get(childRunId)?.completion;
|
|
1716
2415
|
if (completion) {
|
|
1717
2416
|
childStarted = true;
|
|
@@ -1729,37 +2428,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1729
2428
|
label: "Workflow Retry",
|
|
1730
2429
|
description: "Retry a failed workflow run by replaying its completed structural operations",
|
|
1731
2430
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
1732
|
-
async execute(_id, params,
|
|
2431
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1733
2432
|
try {
|
|
1734
|
-
const result = await retryWorkflowRun(params.runId, ctx);
|
|
2433
|
+
const result = await retryWorkflowRun(params.runId, ctx, signal);
|
|
1735
2434
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1736
2435
|
}
|
|
1737
2436
|
catch (error) {
|
|
1738
2437
|
throw mainAgentError(error);
|
|
1739
2438
|
}
|
|
1740
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); },
|
|
1741
2442
|
});
|
|
1742
2443
|
pi.registerTool({
|
|
1743
2444
|
name: "workflow_resume",
|
|
1744
2445
|
label: "Workflow Resume",
|
|
1745
2446
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
1746
2447
|
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
1747
|
-
async execute(_id, params) {
|
|
2448
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1748
2449
|
try {
|
|
1749
|
-
const result = await resumeWorkflowRun(params.runId, params.budget);
|
|
2450
|
+
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
|
|
1750
2451
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1751
2452
|
}
|
|
1752
2453
|
catch (error) {
|
|
1753
2454
|
throw mainAgentError(error);
|
|
1754
2455
|
}
|
|
1755
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); },
|
|
1756
2459
|
});
|
|
1757
2460
|
pi.on("session_start", async (_event, ctx) => {
|
|
1758
2461
|
if (sessionStarted)
|
|
1759
2462
|
return;
|
|
1760
2463
|
sessionStarted = true;
|
|
1761
2464
|
registry.freeze();
|
|
1762
|
-
registerCatalog();
|
|
2465
|
+
registerCatalog(ctx.cwd, projectTrusted(ctx));
|
|
1763
2466
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1764
2467
|
try {
|
|
1765
2468
|
for (const runId of await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)) {
|
|
@@ -1795,7 +2498,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1795
2498
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
1796
2499
|
const abortController = new AbortController();
|
|
1797
2500
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
1798
|
-
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 } : {}) });
|
|
1799
2502
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
1800
2503
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
1801
2504
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -1832,7 +2535,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1832
2535
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1833
2536
|
if (!pi.getActiveTools().includes("workflow"))
|
|
1834
2537
|
return;
|
|
1835
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
2538
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
|
|
1836
2539
|
if (!roles.length)
|
|
1837
2540
|
return;
|
|
1838
2541
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
@@ -1848,32 +2551,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1848
2551
|
try {
|
|
1849
2552
|
const headless = object(ctx) && ctx.headless === true;
|
|
1850
2553
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1851
|
-
const defaults = loadSettings(settingsPath);
|
|
1852
2554
|
if (!ctx.model)
|
|
1853
2555
|
throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
1854
|
-
const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
|
|
1855
2556
|
const budget = validateBudget(params.budget);
|
|
1856
2557
|
const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
1857
2558
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
1858
2559
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
1859
|
-
const
|
|
1860
|
-
knownModels.
|
|
1861
|
-
const availableModels =
|
|
1862
|
-
const rootTools = pi.getActiveTools().filter((name) => !
|
|
2560
|
+
const inventory = modelInventory(rootModel, modelRegistry);
|
|
2561
|
+
const knownModels = inventory.knownModels;
|
|
2562
|
+
const availableModels = inventory.availableModels;
|
|
2563
|
+
const rootTools = pi.getActiveTools().filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1863
2564
|
const trustedProject = projectTrusted(ctx);
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
1867
|
-
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
1868
|
-
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1869
|
-
const runId = randomUUID();
|
|
1870
|
-
const args = (params.args ?? null);
|
|
1871
|
-
encoded(args);
|
|
2565
|
+
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
2566
|
+
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
1872
2567
|
const runController = new AbortController();
|
|
1873
2568
|
if (signal?.aborted)
|
|
1874
2569
|
runController.abort();
|
|
1875
2570
|
else
|
|
1876
2571
|
signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
|
|
2572
|
+
const resolvedAliases = await resolveLaunchAliases(registry, launch.settings.modelAliases ?? {}, { cwd: launchCwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: runController.signal }, availableModels, knownModels, settingsPath);
|
|
2573
|
+
const modelAliases = resolvedAliases.aliases;
|
|
2574
|
+
const settings = Object.freeze({ ...launch.settings, ...(Object.keys(modelAliases).length ? { modelAliases } : {}) });
|
|
2575
|
+
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath }, registry);
|
|
2576
|
+
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
2577
|
+
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
2578
|
+
const runId = randomUUID();
|
|
2579
|
+
const args = (params.args ?? null);
|
|
2580
|
+
encoded(args);
|
|
1877
2581
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
1878
2582
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
1879
2583
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
@@ -1882,9 +2586,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1882
2586
|
await store.validateParentRun(parentRunId);
|
|
1883
2587
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
|
|
1884
2588
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
1885
|
-
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model,
|
|
2589
|
+
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
1886
2590
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
1887
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function", functionName } : {}), ...(
|
|
2591
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
1888
2592
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
1889
2593
|
const initialBudget = budgetRuntime.snapshot();
|
|
1890
2594
|
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
@@ -1893,45 +2597,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1893
2597
|
const providerPause = async () => { if (background)
|
|
1894
2598
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1895
2599
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
1896
|
-
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 });
|
|
1897
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 } : {}) });
|
|
1898
2602
|
if (params.foreground && onUpdate)
|
|
1899
2603
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1900
2604
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
1901
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: async (
|
|
1902
|
-
await lifecycle.enter();
|
|
1903
|
-
try {
|
|
1904
|
-
const path = agentIdentityPath(identity);
|
|
1905
|
-
const replayed = await store.replay(path);
|
|
1906
|
-
if (replayed) {
|
|
1907
|
-
return replayed.value;
|
|
1908
|
-
}
|
|
1909
|
-
const worktree = agentWorktree(identity);
|
|
1910
|
-
const cwd = worktree.worktreeOwner ? (await persistWorktree(store, checked.metadata, worktree.worktreeOwner)).cwd : ctx.cwd;
|
|
1911
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
1912
|
-
const model = typeof options.model === "string" ? options.model : undefined;
|
|
1913
|
-
const thinking = parseThinking(options.thinking);
|
|
1914
|
-
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1915
|
-
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 } : {}) });
|
|
1916
|
-
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
1917
|
-
const tools = resolved.tools;
|
|
1918
|
-
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
1919
|
-
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 });
|
|
1920
|
-
const cancel = () => { scheduler.cancel(spawned.id); };
|
|
1921
|
-
if (agentSignal.aborted)
|
|
1922
|
-
cancel();
|
|
1923
|
-
else
|
|
1924
|
-
agentSignal.addEventListener("abort", cancel, { once: true });
|
|
1925
|
-
const outcome = await spawned.result.finally(() => { agentSignal.removeEventListener("abort", cancel); });
|
|
1926
|
-
if (!outcome.ok)
|
|
1927
|
-
throw new WorkflowError(outcome.error.code, outcome.error.message);
|
|
1928
|
-
await store.complete(path, outcome.value);
|
|
1929
|
-
return outcome.value;
|
|
1930
|
-
}
|
|
1931
|
-
finally {
|
|
1932
|
-
await lifecycle.leave();
|
|
1933
|
-
}
|
|
1934
|
-
}, 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);
|
|
1935
2606
|
runs.get(runId).execution = execution;
|
|
1936
2607
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1937
2608
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2027,7 +2698,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2027
2698
|
return entries.filter((entry) => entry !== undefined);
|
|
2028
2699
|
};
|
|
2029
2700
|
let stores = await loadStores();
|
|
2030
|
-
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or
|
|
2701
|
+
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint-name]. Approve/reject are for checkpoints only; use workflow_respond with a proposalId or the navigator's budget controls for budget decisions. Use workflow_resume for budget patches.";
|
|
2031
2702
|
const setWorkflowStatus = (text) => {
|
|
2032
2703
|
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
2033
2704
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
@@ -2044,12 +2715,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2044
2715
|
return keepContext ? "dashboard" : "done";
|
|
2045
2716
|
}
|
|
2046
2717
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2047
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true);
|
|
2718
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2048
2719
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2049
2720
|
return keepContext ? "dashboard" : "done";
|
|
2050
2721
|
}
|
|
2051
2722
|
if (action === "delete" && stored) {
|
|
2052
|
-
if (!
|
|
2723
|
+
if (!HARD_TERMINAL_RUN_STATES.has(stored.loaded.run.state)) {
|
|
2053
2724
|
ctx.ui.notify("Stop the workflow before deleting it.", "warning");
|
|
2054
2725
|
return keepContext ? "dashboard" : "done";
|
|
2055
2726
|
}
|
|
@@ -2069,7 +2740,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2069
2740
|
if (action === "resume" && run) {
|
|
2070
2741
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2071
2742
|
const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
|
|
2072
|
-
const result = await resumeWorkflowRun(run.store.runId, patch);
|
|
2743
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2073
2744
|
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
|
|
2074
2745
|
}
|
|
2075
2746
|
else {
|
|
@@ -2077,7 +2748,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2077
2748
|
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2078
2749
|
else {
|
|
2079
2750
|
if (run.lifecycle.state === "paused")
|
|
2080
|
-
await refreshPausedRunAliases(run, ctx);
|
|
2751
|
+
await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
2081
2752
|
await run.lifecycle.resume();
|
|
2082
2753
|
}
|
|
2083
2754
|
ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
|
|
@@ -2088,7 +2759,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2088
2759
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
2089
2760
|
if (input === undefined)
|
|
2090
2761
|
return keepContext ? "dashboard" : "done";
|
|
2091
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
2762
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2092
2763
|
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
|
|
2093
2764
|
return keepContext ? "dashboard" : "done";
|
|
2094
2765
|
}
|
|
@@ -2123,6 +2794,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2123
2794
|
};
|
|
2124
2795
|
const manageAliases = async () => {
|
|
2125
2796
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2797
|
+
let aliasSettingsPath = settingsPath;
|
|
2798
|
+
const trustedProject = projectTrusted(ctx);
|
|
2126
2799
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
2127
2800
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
2128
2801
|
const selectTarget = async (aliases) => {
|
|
@@ -2136,22 +2809,24 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2136
2809
|
};
|
|
2137
2810
|
const save = (aliases) => {
|
|
2138
2811
|
try {
|
|
2139
|
-
saveModelAliases(
|
|
2140
|
-
ctx.ui.notify(`Saved model aliases to ${
|
|
2812
|
+
saveModelAliases(aliasSettingsPath, aliases);
|
|
2813
|
+
ctx.ui.notify(`Saved model aliases to ${aliasSettingsPath}.`, "info");
|
|
2141
2814
|
return true;
|
|
2142
2815
|
}
|
|
2143
2816
|
catch (error) {
|
|
2144
|
-
ctx.ui.notify(`${
|
|
2817
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2145
2818
|
return false;
|
|
2146
2819
|
}
|
|
2147
2820
|
};
|
|
2148
2821
|
for (;;) {
|
|
2149
2822
|
let aliases;
|
|
2150
2823
|
try {
|
|
2151
|
-
|
|
2824
|
+
const resolution = resolveWorkflowSettings(ctx.cwd, trustedProject, settingsPath);
|
|
2825
|
+
aliases = resolution.effective.modelAliases ?? {};
|
|
2826
|
+
aliasSettingsPath = resolution.sources.modelAliases;
|
|
2152
2827
|
}
|
|
2153
2828
|
catch (error) {
|
|
2154
|
-
ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2829
|
+
ctx.ui.notify(`${trustedProject ? workflowProjectSettingsPath(ctx.cwd) : settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2155
2830
|
return;
|
|
2156
2831
|
}
|
|
2157
2832
|
const names = Object.keys(aliases).sort();
|
|
@@ -2173,13 +2848,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2173
2848
|
continue;
|
|
2174
2849
|
const next = { ...aliases, [name]: target };
|
|
2175
2850
|
try {
|
|
2176
|
-
validateModelAliases(next,
|
|
2851
|
+
validateModelAliases(next, aliasSettingsPath);
|
|
2177
2852
|
}
|
|
2178
2853
|
catch (error) {
|
|
2179
|
-
ctx.ui.notify(`${
|
|
2854
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2180
2855
|
continue;
|
|
2181
2856
|
}
|
|
2182
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2857
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
2183
2858
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
2184
2859
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
2185
2860
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
|
|
@@ -2195,13 +2870,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2195
2870
|
continue;
|
|
2196
2871
|
const next = { ...aliases, [edit[1]]: target };
|
|
2197
2872
|
try {
|
|
2198
|
-
validateModelAliases(next,
|
|
2873
|
+
validateModelAliases(next, aliasSettingsPath);
|
|
2199
2874
|
}
|
|
2200
2875
|
catch (error) {
|
|
2201
|
-
ctx.ui.notify(`${
|
|
2876
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2202
2877
|
continue;
|
|
2203
2878
|
}
|
|
2204
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2879
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
2205
2880
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
2206
2881
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
2207
2882
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
|
|
@@ -2238,9 +2913,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2238
2913
|
}
|
|
2239
2914
|
const sorted = navigatorAttentionSort(stores);
|
|
2240
2915
|
const labels = navigatorRunLabels(sorted);
|
|
2241
|
-
const terminalStates =
|
|
2916
|
+
const terminalStates = HARD_TERMINAL_RUN_STATES;
|
|
2242
2917
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
2243
|
-
const
|
|
2918
|
+
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
2919
|
+
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
2244
2920
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
2245
2921
|
if (!runChoice || runChoice === "Close")
|
|
2246
2922
|
return;
|
|
@@ -2273,6 +2949,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2273
2949
|
stores = await loadStores();
|
|
2274
2950
|
continue;
|
|
2275
2951
|
}
|
|
2952
|
+
if (runChoice === "Delete all failed") {
|
|
2953
|
+
if (!await ctx.ui.confirm("Delete failed runs?", "Delete all failed workflow runs and their artifacts? This cannot be undone."))
|
|
2954
|
+
continue;
|
|
2955
|
+
for (const entry of sorted) {
|
|
2956
|
+
if (entry.loaded.run.state === "failed") {
|
|
2957
|
+
await entry.store.delete(true);
|
|
2958
|
+
runs.delete(entry.store.runId);
|
|
2959
|
+
terminalRunStates.delete(entry.store.runId);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
ctx.ui.notify("Deleted all failed workflow runs.", "info");
|
|
2963
|
+
stores = await loadStores();
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2276
2966
|
const runIndex = labels.indexOf(runChoice);
|
|
2277
2967
|
if (runIndex < 0)
|
|
2278
2968
|
return;
|
|
@@ -2293,6 +2983,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2293
2983
|
const loaded = await store.load();
|
|
2294
2984
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2295
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
|
+
}
|
|
2296
2995
|
const actions = new Map();
|
|
2297
2996
|
const copies = new Map();
|
|
2298
2997
|
const reviews = new Map();
|
|
@@ -2327,8 +3026,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2327
3026
|
if (ctx.mode !== "tui")
|
|
2328
3027
|
actions.set("Refresh", "refresh");
|
|
2329
3028
|
else
|
|
2330
|
-
actions.set("
|
|
2331
|
-
if (loaded.run.agents.length)
|
|
3029
|
+
actions.set("Open script in editor", "open-script");
|
|
3030
|
+
if (ctx.mode !== "tui" && loaded.run.agents.length)
|
|
2332
3031
|
actions.set("Agents...", "agents");
|
|
2333
3032
|
if (terminalStates.has(loaded.run.state))
|
|
2334
3033
|
add("Delete", "delete");
|
|
@@ -2336,40 +3035,55 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2336
3035
|
addCopy("Copy run path", store.directory, "run path");
|
|
2337
3036
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2338
3037
|
}
|
|
2339
|
-
return { dashboard:
|
|
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 };
|
|
2340
3039
|
};
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
const parent = byId.get(parentId);
|
|
2347
|
-
if (!parent)
|
|
2348
|
-
break;
|
|
2349
|
-
parents.unshift(parent.label ?? parent.name);
|
|
2350
|
-
}
|
|
2351
|
-
return [...((agent.structuralPath ?? []).length ? [(agent.structuralPath ?? []).join(" > ")] : []), ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : []), ...parents, agent.label ?? agent.name].join(" > ");
|
|
2352
|
-
};
|
|
2353
|
-
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
2354
|
-
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
2355
|
-
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
2356
|
-
const selected = selectedIndex >= 0 ? dashboard.agents[selectedIndex] : undefined;
|
|
2357
|
-
if (!selected)
|
|
2358
|
-
return;
|
|
2359
|
-
const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
2360
|
-
const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
|
|
2361
|
-
const actions = [
|
|
2362
|
-
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
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"] : []),
|
|
2363
3045
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3046
|
+
...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
|
|
2364
3047
|
"Copy agent ID",
|
|
2365
3048
|
"Back",
|
|
2366
3049
|
];
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
3050
|
+
};
|
|
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) => {
|
|
3072
|
+
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
3073
|
+
const title = (agent) => agentBreadcrumb(agent, byId, true);
|
|
3074
|
+
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
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
|
+
}
|
|
3083
|
+
if (!selected)
|
|
3084
|
+
return;
|
|
3085
|
+
const worktree = agentWorktreeFor(dashboard, selected);
|
|
3086
|
+
const actions = agentActionLabels(dashboard, selected);
|
|
2373
3087
|
for (;;) {
|
|
2374
3088
|
const action = await ctx.ui.select(title(selected), actions);
|
|
2375
3089
|
if (!action || action === "Back")
|
|
@@ -2386,55 +3100,87 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2386
3100
|
await copyArtifact(worktree.path, "worktree path");
|
|
2387
3101
|
continue;
|
|
2388
3102
|
}
|
|
2389
|
-
if (action === "Fork as Pi session in pane")
|
|
2390
|
-
|
|
2391
|
-
if (!attempt)
|
|
2392
|
-
continue;
|
|
2393
|
-
const running = !SETTLED_AGENT_STATES.has(selected.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
2394
|
-
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?"))
|
|
2395
|
-
continue;
|
|
2396
|
-
try {
|
|
2397
|
-
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
2398
|
-
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
2399
|
-
}
|
|
2400
|
-
catch (error) {
|
|
2401
|
-
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
3103
|
+
if (action === "Fork as Pi session in pane")
|
|
3104
|
+
await forkAgentSession(dashboard, selected);
|
|
2404
3105
|
}
|
|
2405
3106
|
};
|
|
2406
3107
|
for (;;) {
|
|
2407
3108
|
let view = await loadDashboard();
|
|
2408
3109
|
const actionChoice = ctx.mode === "tui"
|
|
2409
3110
|
? await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
2410
|
-
let options = [...view.actions.keys(), "Close"];
|
|
2411
|
-
let selectedIndex = 0;
|
|
2412
3111
|
let dashboardOffset = 0;
|
|
2413
3112
|
let refreshing = false;
|
|
2414
3113
|
let disposed = false;
|
|
3114
|
+
let detailsMode = false;
|
|
3115
|
+
let actionMode = false;
|
|
3116
|
+
let actionIndex = 0;
|
|
2415
3117
|
let stopRequested = false;
|
|
2416
3118
|
let stopStatus;
|
|
3119
|
+
let selectionNeedsScroll = true;
|
|
3120
|
+
let renderedWidth = 80;
|
|
3121
|
+
let refreshGeneration = 0;
|
|
3122
|
+
const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
|
|
3123
|
+
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3124
|
+
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
3125
|
+
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
2417
3126
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
2418
|
-
const keyLabels = { up: "↑", down: "↓",
|
|
3127
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
2419
3128
|
const keyLabel = (binding, fallback) => {
|
|
2420
3129
|
const keys = keybindingKeys(keybindings, binding);
|
|
2421
3130
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
2422
3131
|
};
|
|
2423
|
-
const
|
|
2424
|
-
const
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
const
|
|
2429
|
-
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
|
+
}
|
|
2430
3164
|
};
|
|
2431
|
-
const updateDashboard = async (
|
|
3165
|
+
const updateDashboard = async () => {
|
|
3166
|
+
const generation = ++refreshGeneration;
|
|
3167
|
+
const hadExpandableNodes = tree.nodes.some((node) => node.children.length > 0);
|
|
2432
3168
|
const next = await loadDashboard();
|
|
2433
|
-
if (disposed)
|
|
3169
|
+
if (disposed || generation !== refreshGeneration)
|
|
2434
3170
|
return;
|
|
3171
|
+
const previousNodeId = selectedNodeId;
|
|
3172
|
+
const previousExpanded = expandedNodeIds;
|
|
3173
|
+
const selectedAction = actionMode ? actionOptions()[actionIndex] : undefined;
|
|
2435
3174
|
view = next;
|
|
2436
|
-
|
|
2437
|
-
|
|
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;
|
|
2438
3184
|
tui.requestRender();
|
|
2439
3185
|
};
|
|
2440
3186
|
const requestStop = () => {
|
|
@@ -2443,16 +3189,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2443
3189
|
stopRequested = true;
|
|
2444
3190
|
stopStatus = undefined;
|
|
2445
3191
|
setWorkflowStatus(undefined);
|
|
2446
|
-
const selectedOption = options[selectedIndex];
|
|
2447
3192
|
void runAction(`stop ${store.runId}`, true, (status) => {
|
|
2448
3193
|
stopStatus = status;
|
|
2449
3194
|
setWorkflowStatus(status);
|
|
2450
3195
|
if (!disposed)
|
|
2451
3196
|
tui.requestRender();
|
|
2452
|
-
}).then(() => updateDashboard(
|
|
3197
|
+
}).then(() => updateDashboard()).catch((error) => {
|
|
2453
3198
|
if (disposed)
|
|
2454
3199
|
return;
|
|
2455
3200
|
stopStatus = `Could not stop workflow ${store.runId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3201
|
+
setWorkflowStatus(stopStatus);
|
|
2456
3202
|
tui.requestRender();
|
|
2457
3203
|
}).finally(() => {
|
|
2458
3204
|
stopRequested = false;
|
|
@@ -2464,59 +3210,181 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2464
3210
|
if (refreshing || stopRequested)
|
|
2465
3211
|
return;
|
|
2466
3212
|
refreshing = true;
|
|
2467
|
-
|
|
2468
|
-
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3213
|
+
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
2469
3214
|
}, 1000);
|
|
2470
3215
|
timer.unref();
|
|
2471
3216
|
return borderWorkflowOverlay({
|
|
2472
3217
|
render(width) {
|
|
2473
|
-
|
|
2474
|
-
const
|
|
2475
|
-
const
|
|
2476
|
-
const
|
|
2477
|
-
const
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
3218
|
+
renderedWidth = width;
|
|
3219
|
+
const narrow = width < 80;
|
|
3220
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
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;
|
|
2482
3250
|
}
|
|
2483
|
-
const maxOffset = Math.max(0, dashboardLines.length - layout.dashboardViewport);
|
|
2484
3251
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
2485
|
-
const
|
|
2486
|
-
return [
|
|
2487
|
-
...dashboardLines.slice(dashboardOffset, dashboardOffset + layout.dashboardViewport),
|
|
2488
|
-
...(layout.separatorRows && layout.dashboardViewport ? [""] : []),
|
|
2489
|
-
...actionLines.slice(actionStart, actionStart + layout.actionViewport),
|
|
2490
|
-
...(layout.hintRows ? [hint] : []),
|
|
2491
|
-
];
|
|
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] : [])];
|
|
2492
3254
|
},
|
|
2493
3255
|
invalidate() { },
|
|
2494
3256
|
handleInput(data) {
|
|
2495
|
-
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();
|
|
2496
3265
|
return;
|
|
2497
|
-
if (keybindings.matches(data, "tui.select.up"))
|
|
2498
|
-
selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
2499
|
-
else if (keybindings.matches(data, "tui.select.down"))
|
|
2500
|
-
selectedIndex = (selectedIndex + 1) % options.length;
|
|
2501
|
-
else if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
2502
|
-
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, dashboardLayout().dashboardViewport));
|
|
2503
3266
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
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);
|
|
3312
|
+
}
|
|
3313
|
+
tui.requestRender();
|
|
3314
|
+
return;
|
|
2506
3315
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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;
|
|
3321
|
+
}
|
|
2510
3322
|
else
|
|
2511
|
-
done(
|
|
3323
|
+
done(undefined);
|
|
3324
|
+
}
|
|
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;
|
|
3354
|
+
}
|
|
3355
|
+
else if (keybindings.matches(data, "tui.select.up")) {
|
|
3356
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
3357
|
+
selectedNodeId = next.nodeId;
|
|
3358
|
+
selectionNeedsScroll = true;
|
|
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);
|
|
3369
|
+
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
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
|
+
}
|
|
2512
3382
|
}
|
|
2513
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
2514
|
-
done(undefined);
|
|
2515
3383
|
tui.requestRender();
|
|
2516
3384
|
},
|
|
2517
3385
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
2518
3386
|
}, theme);
|
|
2519
|
-
}, { overlay: true })
|
|
3387
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS })
|
|
2520
3388
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
2521
3389
|
if (!actionChoice || actionChoice === "Close")
|
|
2522
3390
|
return;
|
|
@@ -2524,48 +3392,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2524
3392
|
await selectAgent(view);
|
|
2525
3393
|
continue;
|
|
2526
3394
|
}
|
|
2527
|
-
if (actionChoice
|
|
3395
|
+
if (actionChoice.startsWith("__workflow_agent__:")) {
|
|
3396
|
+
await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
|
|
2528
3397
|
continue;
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
const move = (delta) => {
|
|
2536
|
-
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
2537
|
-
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
2538
|
-
};
|
|
2539
|
-
return borderWorkflowOverlay({
|
|
2540
|
-
render(width) {
|
|
2541
|
-
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
2542
|
-
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
2543
|
-
offset = Math.min(offset, maxOffset);
|
|
2544
|
-
return [
|
|
2545
|
-
theme.fg("accent", "Workflow script"),
|
|
2546
|
-
...renderedLines.slice(offset, offset + viewport()),
|
|
2547
|
-
"",
|
|
2548
|
-
theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close"),
|
|
2549
|
-
];
|
|
2550
|
-
},
|
|
2551
|
-
invalidate() { },
|
|
2552
|
-
handleInput(data) {
|
|
2553
|
-
if (keybindings.matches(data, "tui.select.up"))
|
|
2554
|
-
move(-1);
|
|
2555
|
-
else if (keybindings.matches(data, "tui.select.down"))
|
|
2556
|
-
move(1);
|
|
2557
|
-
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
2558
|
-
move(-viewport());
|
|
2559
|
-
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
2560
|
-
move(viewport());
|
|
2561
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
2562
|
-
done(undefined);
|
|
2563
|
-
tui.requestRender();
|
|
2564
|
-
},
|
|
2565
|
-
}, theme);
|
|
2566
|
-
}, { 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);
|
|
2567
3404
|
continue;
|
|
2568
3405
|
}
|
|
3406
|
+
if (actionChoice === "Refresh")
|
|
3407
|
+
continue;
|
|
2569
3408
|
const copy = view.copies.get(actionChoice);
|
|
2570
3409
|
if (copy) {
|
|
2571
3410
|
await copyArtifact(copy.value, copy.artifact);
|
|
@@ -2629,7 +3468,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2629
3468
|
tui.requestRender();
|
|
2630
3469
|
},
|
|
2631
3470
|
}, theme);
|
|
2632
|
-
}, { overlay: true, overlayOptions:
|
|
3471
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS });
|
|
2633
3472
|
if (decision) {
|
|
2634
3473
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
2635
3474
|
if (!accepted)
|
|
@@ -2656,16 +3495,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2656
3495
|
pi.on("session_shutdown", async () => {
|
|
2657
3496
|
try {
|
|
2658
3497
|
await Promise.all([...runs.entries()].map(async ([runId, run]) => {
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
return;
|
|
2662
|
-
}
|
|
2663
|
-
if (!["completed", "failed", "stopped", "budget_exhausted"].includes(run.lifecycle.state)) {
|
|
3498
|
+
const isTerminal = SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state);
|
|
3499
|
+
if (!isTerminal) {
|
|
2664
3500
|
try {
|
|
2665
3501
|
await run.lifecycle.terminal("interrupted");
|
|
2666
3502
|
}
|
|
2667
3503
|
catch (error) {
|
|
2668
|
-
if (!
|
|
3504
|
+
if (!SHUTDOWN_TERMINAL_RUN_STATES.has(run.lifecycle.state))
|
|
2669
3505
|
throw error;
|
|
2670
3506
|
}
|
|
2671
3507
|
run.abortController.abort();
|