pi-extensible-workflows 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -0
- package/src/workflow-evals.ts +24 -3
package/dist/src/host.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { Type } from "@earendil-works/pi-ai";
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import { FairAgentScheduler, WorkflowAgentExecutor, localAgentTransport } from "./agent-execution.js";
|
|
11
11
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
12
12
|
import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
13
13
|
import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
@@ -15,8 +15,8 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
|
|
|
15
15
|
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
16
16
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
|
|
17
17
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
18
|
-
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
|
|
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
|
+
import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
|
|
19
|
+
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STALL_THRESHOLD_MS, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
|
|
20
20
|
const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
|
|
21
21
|
const INTERNAL_WORKFLOW_TOOLS = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
|
|
22
22
|
const HARD_TERMINAL_RUN_STATES = new Set(["completed", "failed", "stopped"]);
|
|
@@ -88,6 +88,8 @@ function mainAgentError(error) {
|
|
|
88
88
|
Object.assign(presented, typed);
|
|
89
89
|
return presented;
|
|
90
90
|
}
|
|
91
|
+
function workflowFailedAt(error) { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
|
|
92
|
+
function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
91
93
|
export class RunLifecycle {
|
|
92
94
|
changed;
|
|
93
95
|
#state;
|
|
@@ -163,14 +165,14 @@ export class RunLifecycle {
|
|
|
163
165
|
export function formatWorkflowPreview(args) {
|
|
164
166
|
const explicitName = typeof args.name === "string" && args.name.trim() ? args.name.trim() : undefined;
|
|
165
167
|
const registeredName = typeof args.workflow === "string" && args.workflow.trim() ? args.workflow.trim() : undefined;
|
|
166
|
-
const name =
|
|
168
|
+
const name = explicitName ?? registeredName ?? "workflow";
|
|
167
169
|
if (typeof args.script !== "string" || !args.script.trim())
|
|
168
|
-
return `workflow ${name}${registeredName ?
|
|
170
|
+
return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
|
|
169
171
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
170
172
|
}
|
|
171
173
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
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
|
|
174
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
|
|
175
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline and file-backed launches require a non-empty name; registered function launches may use name as an optional run label and otherwise use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
174
176
|
function workflowRecoveryGuidance(action, state) {
|
|
175
177
|
if (action === "resume") {
|
|
176
178
|
if (state === "failed")
|
|
@@ -194,17 +196,18 @@ function workflowRecoveryGuidance(action, state) {
|
|
|
194
196
|
return `Only failed workflow runs can be retried; source is ${state}`;
|
|
195
197
|
}
|
|
196
198
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
197
|
-
name: Type.Optional(Type.String({ description: "
|
|
199
|
+
name: Type.Optional(Type.String({ description: "Optional run label; required and non-empty for inline or file-backed launches, defaults to the registered function name when omitted" })),
|
|
198
200
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
199
201
|
script: Type.Optional(Type.String({ description: "Immutable inline workflow source; default to a named script that fans out with parallel(...) and awaits results before passing them to a summarizing agent(...)" })),
|
|
200
|
-
|
|
202
|
+
scriptPath: Type.Optional(Type.String({ description: "Path to a JavaScript workflow file, read once at launch and persisted as the inline source" })),
|
|
203
|
+
workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name; name may optionally label the run" })),
|
|
201
204
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
202
205
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
|
|
203
206
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
|
|
204
207
|
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
205
208
|
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
206
209
|
});
|
|
207
|
-
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
210
|
+
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
208
211
|
function phaseNames(source) {
|
|
209
212
|
const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
|
|
210
213
|
return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
@@ -547,19 +550,36 @@ function styleForState(map, state, styles) {
|
|
|
547
550
|
function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
548
551
|
function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
549
552
|
function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
550
|
-
|
|
553
|
+
function formatWorkflowRuntime(durationMs) {
|
|
554
|
+
const seconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
555
|
+
if (seconds < 60)
|
|
556
|
+
return `${String(seconds)}s`;
|
|
557
|
+
const minutes = Math.floor(seconds / 60);
|
|
558
|
+
const remainingSeconds = seconds % 60;
|
|
559
|
+
if (minutes < 60)
|
|
560
|
+
return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
|
|
561
|
+
const hours = Math.floor(minutes / 60);
|
|
562
|
+
const remainingMinutes = minutes % 60;
|
|
563
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
564
|
+
}
|
|
565
|
+
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
551
566
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
552
567
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
553
568
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
554
569
|
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
555
|
-
const
|
|
570
|
+
const state = progressStyleForState(run.state, styles)(`[${run.state}]`);
|
|
571
|
+
const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
572
|
+
const lines = [`${iconStyle(workflowIcon)} ${header} ${state}${runtime}`];
|
|
556
573
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
557
574
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
575
|
+
const activeShells = run.activeShells ?? 0;
|
|
576
|
+
if (activeShells > 0)
|
|
577
|
+
lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
|
|
558
578
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
559
579
|
const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
560
580
|
const icon = agentStateGlyph(agent.state, spinner);
|
|
561
581
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
562
|
-
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
582
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
563
583
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
564
584
|
const state = progressStyleForState(agent.state, styles);
|
|
565
585
|
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
|
|
@@ -581,6 +601,7 @@ function workflowToolUpdate(run) {
|
|
|
581
601
|
return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
|
|
582
602
|
}
|
|
583
603
|
const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
604
|
+
const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
|
|
584
605
|
function textBlock(text) {
|
|
585
606
|
return {
|
|
586
607
|
render(width) {
|
|
@@ -623,7 +644,7 @@ function controlState(state, theme) {
|
|
|
623
644
|
return theme.fg(color, state);
|
|
624
645
|
}
|
|
625
646
|
function controlAction(action, theme) {
|
|
626
|
-
const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
647
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
627
648
|
return theme.fg(color, action);
|
|
628
649
|
}
|
|
629
650
|
function budgetPatchEntries(value) {
|
|
@@ -681,14 +702,15 @@ function workflowControlResult(name, args, result, expanded, theme, isError) {
|
|
|
681
702
|
if (name === "workflow_retry") {
|
|
682
703
|
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
683
704
|
const state = controlString(value.state) ?? "unknown";
|
|
705
|
+
const action = state === "completed" ? "completed" : "started";
|
|
684
706
|
if (!expanded)
|
|
685
|
-
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(
|
|
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");
|
|
707
|
+
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
708
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
|
|
687
709
|
}
|
|
688
710
|
if (name === "workflow_resume") {
|
|
689
711
|
const state = controlString(value.state) ?? "unknown";
|
|
690
712
|
const proposalId = controlString(value.proposalId);
|
|
691
|
-
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
|
|
713
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
692
714
|
if (!expanded)
|
|
693
715
|
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
694
716
|
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");
|
|
@@ -809,14 +831,41 @@ function themeWorkflowProgressStyles(theme) {
|
|
|
809
831
|
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
810
832
|
};
|
|
811
833
|
}
|
|
812
|
-
function workflowProgressBlock(run, theme) {
|
|
834
|
+
function workflowProgressBlock(run, theme, progress, refresh, invalidate) {
|
|
813
835
|
const styles = themeWorkflowProgressStyles(theme);
|
|
836
|
+
const currentRun = () => {
|
|
837
|
+
const displayed = progress?.run ?? run;
|
|
838
|
+
if (!progress || displayed.state !== "running")
|
|
839
|
+
return displayed;
|
|
840
|
+
const durationMs = Math.max(displayed.usage?.durationMs ?? 0, progress.runtimeBaseMs + Date.now() - progress.runtimeStartedAt);
|
|
841
|
+
return { ...displayed, usage: { ...budgetUsage(displayed.usage), durationMs } };
|
|
842
|
+
};
|
|
814
843
|
return {
|
|
815
844
|
render(width) {
|
|
816
845
|
const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
|
|
817
|
-
return truncateWorkflowProgress(formatWorkflowProgress(
|
|
846
|
+
return truncateWorkflowProgress(formatWorkflowProgress(currentRun(), frame, styles), width);
|
|
847
|
+
},
|
|
848
|
+
invalidate() {
|
|
849
|
+
const displayed = currentRun();
|
|
850
|
+
if (!progress || !refresh || displayed.state !== "running" || !displayed.agents.some((agent) => agent.state === "running"))
|
|
851
|
+
return;
|
|
852
|
+
const now = Date.now();
|
|
853
|
+
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
|
|
854
|
+
return;
|
|
855
|
+
progress.lastRefreshAt = now;
|
|
856
|
+
const inputRun = progress.inputRun;
|
|
857
|
+
const pending = refresh().then((next) => {
|
|
858
|
+
if (next && progress.inputRun === inputRun) {
|
|
859
|
+
progress.run = next;
|
|
860
|
+
invalidate?.();
|
|
861
|
+
}
|
|
862
|
+
}).catch(() => undefined);
|
|
863
|
+
progress.refresh = pending;
|
|
864
|
+
void pending.finally(() => {
|
|
865
|
+
if (progress.refresh === pending)
|
|
866
|
+
delete progress.refresh;
|
|
867
|
+
});
|
|
818
868
|
},
|
|
819
|
-
invalidate() { },
|
|
820
869
|
};
|
|
821
870
|
}
|
|
822
871
|
export function formatBudgetStatus(run) {
|
|
@@ -888,15 +937,41 @@ function styledAgentBreadcrumb(agent, byId, styles) {
|
|
|
888
937
|
return parts[0] ?? "";
|
|
889
938
|
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
890
939
|
}
|
|
891
|
-
function
|
|
940
|
+
export function formatStalledDuration(durationMs) {
|
|
941
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
942
|
+
if (minutes < 60)
|
|
943
|
+
return `${String(minutes)}m`;
|
|
944
|
+
const hours = Math.floor(minutes / 60);
|
|
945
|
+
const remainingMinutes = minutes % 60;
|
|
946
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
947
|
+
}
|
|
948
|
+
function stalledDuration(agent, now) {
|
|
949
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
|
|
950
|
+
return undefined;
|
|
951
|
+
const duration = now - agent.lastEventAt;
|
|
952
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
953
|
+
}
|
|
954
|
+
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
892
955
|
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 ?? "";
|
|
893
|
-
|
|
956
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
957
|
+
const stalled = stalledDuration(agent, now);
|
|
958
|
+
if (stalled === undefined)
|
|
959
|
+
return activity;
|
|
960
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
961
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
962
|
+
}
|
|
963
|
+
function formatAccountingValue(value) {
|
|
964
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
894
965
|
}
|
|
895
966
|
function formatAccounting(accounting) {
|
|
896
967
|
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
897
|
-
return `${
|
|
968
|
+
return `${formatAccountingValue(total)} tok`;
|
|
969
|
+
}
|
|
970
|
+
function formatAgentAccounting(accounting) {
|
|
971
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
972
|
+
return [`Tokens: ${formatAccountingValue(total)} (in=${formatAccountingValue(accounting.input)} out=${formatAccountingValue(accounting.output)} cache-read=${formatAccountingValue(accounting.cacheRead)} cache-write=${formatAccountingValue(accounting.cacheWrite)})`, `Cost: $${accounting.cost.toFixed(2)}`];
|
|
898
973
|
}
|
|
899
|
-
export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
974
|
+
export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
|
|
900
975
|
void worktrees;
|
|
901
976
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
902
977
|
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 });
|
|
@@ -922,7 +997,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
922
997
|
if (last?.error)
|
|
923
998
|
result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
924
999
|
}
|
|
925
|
-
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
|
|
1000
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
926
1001
|
if (activity)
|
|
927
1002
|
result.push(`${indent} ${activity}`);
|
|
928
1003
|
return result.join("\n");
|
|
@@ -935,7 +1010,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
935
1010
|
}
|
|
936
1011
|
return lines.join("\n");
|
|
937
1012
|
}
|
|
938
|
-
export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
1013
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
|
|
939
1014
|
const { run, snapshot } = loaded;
|
|
940
1015
|
const lines = [
|
|
941
1016
|
`Workflow: ${run.workflowName}`,
|
|
@@ -971,6 +1046,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
|
971
1046
|
result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
972
1047
|
for (const call of agent.toolCalls ?? [])
|
|
973
1048
|
result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
1049
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
1050
|
+
if (activity)
|
|
1051
|
+
result.push(`${indent} ${activity}`);
|
|
974
1052
|
return result.join("\n");
|
|
975
1053
|
}));
|
|
976
1054
|
lines.push("Checkpoints:");
|
|
@@ -979,10 +1057,10 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
|
979
1057
|
for (const checkpoint of checkpoints)
|
|
980
1058
|
lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
981
1059
|
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
982
|
-
lines.push(`
|
|
1060
|
+
lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
|
|
983
1061
|
return lines.join("\n");
|
|
984
1062
|
}
|
|
985
|
-
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
1063
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
986
1064
|
const safeWidth = Math.max(1, width);
|
|
987
1065
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
988
1066
|
const tree = buildWorkflowPhaseTree(model);
|
|
@@ -1022,7 +1100,8 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
1022
1100
|
const treeLine = (node) => {
|
|
1023
1101
|
const selected = node.id === selectedNode?.id;
|
|
1024
1102
|
const state = progressStyleForState(node.state, styles);
|
|
1025
|
-
|
|
1103
|
+
const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
|
|
1104
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
|
|
1026
1105
|
};
|
|
1027
1106
|
const details = (node) => {
|
|
1028
1107
|
if (!node)
|
|
@@ -1041,12 +1120,15 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
1041
1120
|
if (!agent)
|
|
1042
1121
|
return [styles.muted("Agent details are unavailable")];
|
|
1043
1122
|
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")])];
|
|
1123
|
+
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)"}`, ...(agent.systemPrompt === undefined ? [] : [`System prompt: ${agent.systemPrompt}`]), `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
1045
1124
|
const error = agent.attemptDetails?.at(-1)?.error;
|
|
1046
1125
|
if (error)
|
|
1047
1126
|
result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
1048
1127
|
if (agent.activity)
|
|
1049
1128
|
result.push(`Activity: ${agent.activity.text}`);
|
|
1129
|
+
const stalled = stalledDuration(agent, now);
|
|
1130
|
+
if (stalled !== undefined)
|
|
1131
|
+
result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
|
|
1050
1132
|
return result;
|
|
1051
1133
|
};
|
|
1052
1134
|
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
@@ -1130,8 +1212,8 @@ function boundedWorkflowFailureDiagnostics(value) {
|
|
|
1130
1212
|
...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
|
|
1131
1213
|
structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
1132
1214
|
attempt: value.failedAgent.attempt,
|
|
1133
|
-
...(value.failedAgent.
|
|
1134
|
-
...(value.failedAgent.
|
|
1215
|
+
...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
|
|
1216
|
+
...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
|
|
1135
1217
|
} } : {}),
|
|
1136
1218
|
completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
|
|
1137
1219
|
id: utf8Prefix(agent.id, 128),
|
|
@@ -1160,18 +1242,6 @@ function boundedWorkflowFailureDiagnostics(value) {
|
|
|
1160
1242
|
bounded = { ...bounded, retry };
|
|
1161
1243
|
continue;
|
|
1162
1244
|
}
|
|
1163
|
-
if (bounded.failedAgent?.sessionFile) {
|
|
1164
|
-
const failedAgent = { ...bounded.failedAgent };
|
|
1165
|
-
delete failedAgent.sessionFile;
|
|
1166
|
-
bounded = { ...bounded, failedAgent };
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
if (bounded.failedAgent?.sessionId) {
|
|
1170
|
-
const failedAgent = { ...bounded.failedAgent };
|
|
1171
|
-
delete failedAgent.sessionId;
|
|
1172
|
-
bounded = { ...bounded, failedAgent };
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
1245
|
if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) {
|
|
1176
1246
|
bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } };
|
|
1177
1247
|
continue;
|
|
@@ -1244,8 +1314,8 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
|
|
|
1244
1314
|
...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
|
|
1245
1315
|
structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
|
|
1246
1316
|
attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
|
|
1247
|
-
...(failedAttempt?.
|
|
1248
|
-
...(failedAttempt?.
|
|
1317
|
+
...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
|
|
1318
|
+
...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
|
|
1249
1319
|
} : undefined;
|
|
1250
1320
|
const completedSiblingAgents = run.agents.filter((agent) => {
|
|
1251
1321
|
if (agent.state !== "completed" || agent.id === failedAgentRecord?.id)
|
|
@@ -1282,21 +1352,41 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
|
|
|
1282
1352
|
});
|
|
1283
1353
|
}
|
|
1284
1354
|
export function formatWorkflowFailureDiagnostics(diagnostic) {
|
|
1285
|
-
const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.
|
|
1355
|
+
const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.session ? ` session=${diagnostic.failedAgent.session.transport}/${diagnostic.failedAgent.session.sessionId}` : ""}` : "(not persisted)";
|
|
1286
1356
|
const siblingAgents = diagnostic.completedSiblingAgents;
|
|
1287
1357
|
const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
|
|
1288
1358
|
const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
|
|
1289
1359
|
return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
|
|
1290
1360
|
}
|
|
1361
|
+
function deliveryPart(value, maxBytes) { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
|
|
1362
|
+
export function formatWorkflowFailureDelivery(diagnostic) {
|
|
1363
|
+
const name = deliveryPart(diagnostic.workflowName, 128);
|
|
1364
|
+
const runId = deliveryPart(diagnostic.runId, 128);
|
|
1365
|
+
const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
|
|
1366
|
+
const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
|
|
1367
|
+
const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
|
|
1368
|
+
const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
|
|
1369
|
+
const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
|
|
1370
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1371
|
+
}
|
|
1372
|
+
function formatWorkflowFailureDeliveryFallback(workflowName, runId, runDirectory, error) {
|
|
1373
|
+
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
1374
|
+
const failedPath = workflowFailedAt(error);
|
|
1375
|
+
const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
|
|
1376
|
+
const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
|
|
1377
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1378
|
+
}
|
|
1291
1379
|
function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
|
|
1292
1380
|
function isWorkflowFailureDiagnostics(value) {
|
|
1293
1381
|
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
1294
1382
|
}
|
|
1295
1383
|
function deliver(pi, content) {
|
|
1384
|
+
if (typeof pi.sendMessage !== "function")
|
|
1385
|
+
return;
|
|
1296
1386
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1297
1387
|
}
|
|
1298
1388
|
function deliverFailure(pi, diagnostic) {
|
|
1299
|
-
deliver(pi,
|
|
1389
|
+
deliver(pi, formatWorkflowFailureDelivery(diagnostic));
|
|
1300
1390
|
}
|
|
1301
1391
|
function safeEventError(error) {
|
|
1302
1392
|
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
@@ -1633,6 +1723,7 @@ function tuiRows(tui) {
|
|
|
1633
1723
|
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1634
1724
|
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1635
1725
|
}
|
|
1726
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
1636
1727
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1637
1728
|
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1638
1729
|
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
|
|
@@ -1649,7 +1740,15 @@ function keybindingKeys(keybindings, name) {
|
|
|
1649
1740
|
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
|
|
1650
1741
|
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1651
1742
|
}
|
|
1652
|
-
|
|
1743
|
+
const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
1744
|
+
function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
1745
|
+
function workflowKeyLabel(keybindings, binding, fallback, labels) {
|
|
1746
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
1747
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
1748
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
1749
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
1750
|
+
}
|
|
1751
|
+
export default function workflowExtension(pi, home, clipboard = copyToClipboard, transport = localAgentTransport, agentDir, additionalSkillPaths = []) {
|
|
1653
1752
|
beginWorkflowExtensionLoading();
|
|
1654
1753
|
const registry = loadingRegistry();
|
|
1655
1754
|
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
@@ -1702,16 +1801,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1702
1801
|
});
|
|
1703
1802
|
};
|
|
1704
1803
|
const pendingFailureDiagnostics = new Map();
|
|
1705
|
-
|
|
1706
|
-
if (event.toolName !== "workflow" || !event.isError)
|
|
1707
|
-
return;
|
|
1708
|
-
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1709
|
-
if (!diagnostic)
|
|
1710
|
-
return;
|
|
1711
|
-
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1712
|
-
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1713
|
-
});
|
|
1804
|
+
const foregroundDeliveries = new Map();
|
|
1714
1805
|
const liveActivities = new Map();
|
|
1806
|
+
const liveEventTimes = new Map();
|
|
1807
|
+
const liveAgentSessions = new Map();
|
|
1808
|
+
const setLiveAgentSession = (runId, agentId, session) => { const key = `${runId}:${agentId}`; if (session)
|
|
1809
|
+
liveAgentSessions.set(key, session);
|
|
1810
|
+
else
|
|
1811
|
+
liveAgentSessions.delete(key); };
|
|
1715
1812
|
const setLiveActivity = (runId, agentId, activity) => {
|
|
1716
1813
|
const activities = liveActivities.get(runId);
|
|
1717
1814
|
if (activity) {
|
|
@@ -1726,12 +1823,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1726
1823
|
liveActivities.delete(runId);
|
|
1727
1824
|
}
|
|
1728
1825
|
};
|
|
1826
|
+
const setLiveEventTime = (runId, agentId, timestamp) => {
|
|
1827
|
+
if (timestamp === undefined)
|
|
1828
|
+
return;
|
|
1829
|
+
const timestamps = liveEventTimes.get(runId);
|
|
1830
|
+
if (timestamps)
|
|
1831
|
+
timestamps.set(agentId, timestamp);
|
|
1832
|
+
else
|
|
1833
|
+
liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
|
|
1834
|
+
};
|
|
1729
1835
|
const withLiveActivities = (run) => {
|
|
1730
1836
|
const activities = liveActivities.get(run.id);
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1837
|
+
const timestamps = liveEventTimes.get(run.id);
|
|
1838
|
+
if (!activities?.size && !timestamps?.size)
|
|
1839
|
+
return run;
|
|
1840
|
+
return { ...run, agents: run.agents.map((agent) => {
|
|
1841
|
+
const activity = activities?.get(agent.id);
|
|
1842
|
+
const lastEventAt = timestamps?.get(agent.id);
|
|
1843
|
+
if (activity === undefined && lastEventAt === undefined)
|
|
1844
|
+
return agent;
|
|
1845
|
+
return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1846
|
+
}) };
|
|
1735
1847
|
};
|
|
1736
1848
|
const terminalRunStates = new Map();
|
|
1737
1849
|
let sessionLease;
|
|
@@ -1759,6 +1871,52 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1759
1871
|
await eventPublisher.budget(store, metadata, persisted);
|
|
1760
1872
|
return persisted;
|
|
1761
1873
|
};
|
|
1874
|
+
pi.on("tool_result", async (event) => {
|
|
1875
|
+
const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
|
|
1876
|
+
if (delivery) {
|
|
1877
|
+
if (delivery.timer)
|
|
1878
|
+
clearTimeout(delivery.timer);
|
|
1879
|
+
delivery.inline = true;
|
|
1880
|
+
await delivery.store.updateState((current) => {
|
|
1881
|
+
if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered")
|
|
1882
|
+
return current;
|
|
1883
|
+
return { ...current, delivery: { ...current.delivery, state: "delivered" } };
|
|
1884
|
+
});
|
|
1885
|
+
foregroundDeliveries.delete(event.toolCallId);
|
|
1886
|
+
}
|
|
1887
|
+
if (event.toolName !== "workflow" || !event.isError)
|
|
1888
|
+
return;
|
|
1889
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1890
|
+
if (!diagnostic)
|
|
1891
|
+
return;
|
|
1892
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1893
|
+
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1894
|
+
});
|
|
1895
|
+
const deliverTerminal = async (store, content) => {
|
|
1896
|
+
let claimed;
|
|
1897
|
+
await store.updateState((current) => {
|
|
1898
|
+
if (current.delivery?.state === "delivered")
|
|
1899
|
+
return current;
|
|
1900
|
+
if (!current.delivery) {
|
|
1901
|
+
claimed = true;
|
|
1902
|
+
return current;
|
|
1903
|
+
}
|
|
1904
|
+
claimed = true;
|
|
1905
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
|
|
1906
|
+
});
|
|
1907
|
+
if (claimed === true)
|
|
1908
|
+
deliver(pi, content);
|
|
1909
|
+
};
|
|
1910
|
+
const scheduleForegroundDelivery = (toolCallId, send) => {
|
|
1911
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
1912
|
+
if (!delivery || delivery.inline || typeof pi.sendMessage !== "function")
|
|
1913
|
+
return;
|
|
1914
|
+
//NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
|
|
1915
|
+
delivery.timer = setTimeout(() => {
|
|
1916
|
+
delete delivery.timer;
|
|
1917
|
+
void send().finally(() => foregroundDeliveries.delete(toolCallId));
|
|
1918
|
+
}, 0);
|
|
1919
|
+
};
|
|
1762
1920
|
const phaseBridge = (store, metadata, lifecycle) => {
|
|
1763
1921
|
let cursor = 0;
|
|
1764
1922
|
return async (phase) => {
|
|
@@ -1811,10 +1969,25 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1811
1969
|
const replayed = await store.replay(path);
|
|
1812
1970
|
if (replayed)
|
|
1813
1971
|
return readShellResult(replayed.value);
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1972
|
+
const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
|
|
1973
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
|
|
1974
|
+
try {
|
|
1975
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
1976
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
1977
|
+
await store.complete(path, result);
|
|
1978
|
+
return result;
|
|
1979
|
+
}
|
|
1980
|
+
finally {
|
|
1981
|
+
const stopped = await persistRunState(store, metadata, (current) => {
|
|
1982
|
+
const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
|
|
1983
|
+
if (activeShells > 0)
|
|
1984
|
+
return { ...current, activeShells };
|
|
1985
|
+
const next = { ...current };
|
|
1986
|
+
delete next.activeShells;
|
|
1987
|
+
return next;
|
|
1988
|
+
});
|
|
1989
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
|
|
1990
|
+
}
|
|
1818
1991
|
}
|
|
1819
1992
|
finally {
|
|
1820
1993
|
await lifecycle.leave();
|
|
@@ -1825,8 +1998,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1825
1998
|
budget.transition(next);
|
|
1826
1999
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
1827
2000
|
const nextRun = { ...current, state: next, ...budget.snapshot() };
|
|
1828
|
-
if (next === "running" || next === "completed")
|
|
2001
|
+
if (next === "running" || next === "completed") {
|
|
1829
2002
|
delete nextRun.error;
|
|
2003
|
+
delete nextRun.failedAt;
|
|
2004
|
+
}
|
|
1830
2005
|
return nextRun;
|
|
1831
2006
|
});
|
|
1832
2007
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
@@ -1841,28 +2016,32 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1841
2016
|
const onProgress = async (progress) => {
|
|
1842
2017
|
let runState;
|
|
1843
2018
|
if (progress.persist) {
|
|
1844
|
-
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) } : current);
|
|
2019
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
|
|
1845
2020
|
}
|
|
1846
2021
|
else {
|
|
1847
2022
|
const loaded = await run.store.load();
|
|
1848
2023
|
if (!loaded.run.agents.some((agent) => agent.id === id))
|
|
1849
2024
|
return;
|
|
1850
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
|
|
2025
|
+
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
|
|
1851
2026
|
}
|
|
1852
2027
|
if (!runState.agents.some((agent) => agent.id === id))
|
|
1853
2028
|
return;
|
|
1854
2029
|
setLiveActivity(runId, id, progress.activity);
|
|
2030
|
+
setLiveEventTime(runId, id, progress.lastEventAt);
|
|
1855
2031
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
1856
2032
|
};
|
|
1857
2033
|
const onAttempt = async (attempt) => {
|
|
2034
|
+
setLiveAgentSession(runId, id, attempt.liveSession);
|
|
1858
2035
|
await scheduler.flush();
|
|
1859
2036
|
scheduler.attemptStarted(id);
|
|
2037
|
+
const lastEventAt = Date.now();
|
|
2038
|
+
setLiveEventTime(runId, id, lastEventAt);
|
|
1860
2039
|
await scheduler.flush();
|
|
1861
2040
|
const before = (await run.store.load()).run;
|
|
1862
2041
|
await persistActiveAgentAttempt(run.store, id, attempt);
|
|
1863
2042
|
const active = (await run.store.load()).run;
|
|
1864
2043
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
1865
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2044
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
1866
2045
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1867
2046
|
};
|
|
1868
2047
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
@@ -1872,10 +2051,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1872
2051
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
|
|
1873
2052
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1874
2053
|
setLiveActivity(runId, id);
|
|
2054
|
+
setLiveAgentSession(runId, id);
|
|
1875
2055
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1876
2056
|
return result.value;
|
|
1877
2057
|
}
|
|
1878
2058
|
catch (error) {
|
|
2059
|
+
setLiveAgentSession(runId, id);
|
|
1879
2060
|
const attempts = error.attempts;
|
|
1880
2061
|
if (attempts?.length) {
|
|
1881
2062
|
const before = (await run.store.load()).run;
|
|
@@ -1908,7 +2089,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1908
2089
|
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 };
|
|
1909
2090
|
}
|
|
1910
2091
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
1911
|
-
|
|
2092
|
+
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
|
|
2093
|
+
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1912
2094
|
});
|
|
1913
2095
|
return { ...current, agents };
|
|
1914
2096
|
});
|
|
@@ -1927,6 +2109,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1927
2109
|
terminalRunStates.set(runId, run.lifecycle.state);
|
|
1928
2110
|
run.checkpointResolvers.clear();
|
|
1929
2111
|
liveActivities.delete(runId);
|
|
2112
|
+
liveEventTimes.delete(runId);
|
|
2113
|
+
for (const key of liveAgentSessions.keys())
|
|
2114
|
+
if (key.startsWith(`${runId}:`))
|
|
2115
|
+
liveAgentSessions.delete(key);
|
|
1930
2116
|
eventPublisher.removeRun(runId);
|
|
1931
2117
|
runs.delete(runId);
|
|
1932
2118
|
};
|
|
@@ -1967,7 +2153,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1967
2153
|
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) });
|
|
1968
2154
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1969
2155
|
};
|
|
1970
|
-
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
|
|
2156
|
+
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
|
|
1971
2157
|
const run = runs.get(runId);
|
|
1972
2158
|
if (!run)
|
|
1973
2159
|
return undefined;
|
|
@@ -1975,7 +2161,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1975
2161
|
if (!request)
|
|
1976
2162
|
return undefined;
|
|
1977
2163
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1978
|
-
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
2164
|
+
const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
|
|
1979
2165
|
if (!silent)
|
|
1980
2166
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1981
2167
|
return result;
|
|
@@ -2088,7 +2274,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2088
2274
|
pi.registerTool({
|
|
2089
2275
|
name: "workflow_catalog",
|
|
2090
2276
|
label: "Workflow Catalog",
|
|
2091
|
-
description: "List reusable workflow functions, variables, and model aliases
|
|
2277
|
+
description: "List reusable workflow functions, variables, and model aliases; pass `name` to load one entry in full",
|
|
2092
2278
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
2093
2279
|
async execute(_id, params = {}) {
|
|
2094
2280
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
@@ -2105,7 +2291,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2105
2291
|
});
|
|
2106
2292
|
catalogRegistered = true;
|
|
2107
2293
|
};
|
|
2108
|
-
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() },
|
|
2294
|
+
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, transport);
|
|
2109
2295
|
const activeSnapshotTools = (tools, active) => active === "session"
|
|
2110
2296
|
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
2111
2297
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
@@ -2135,7 +2321,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2135
2321
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2136
2322
|
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
2137
2323
|
};
|
|
2138
|
-
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => async (prompt, options, agentSignal, identity) => {
|
|
2324
|
+
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId, captureRole) => async (prompt, options, agentSignal, identity) => {
|
|
2139
2325
|
await lifecycle.enter();
|
|
2140
2326
|
try {
|
|
2141
2327
|
const path = agentIdentityPath(identity);
|
|
@@ -2150,6 +2336,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2150
2336
|
const thinking = parseThinking(options.thinking);
|
|
2151
2337
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
2152
2338
|
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
2339
|
+
if (role)
|
|
2340
|
+
await captureRole?.(role, resolved.model);
|
|
2153
2341
|
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
2154
2342
|
const tools = resolved.tools;
|
|
2155
2343
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
@@ -2182,8 +2370,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2182
2370
|
if (drift.length)
|
|
2183
2371
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2184
2372
|
};
|
|
2185
|
-
const
|
|
2373
|
+
const recoveryUi = (context) => {
|
|
2374
|
+
const host = object(context) ? context : undefined;
|
|
2375
|
+
const ui = host && object(host.ui) ? host.ui : {};
|
|
2376
|
+
return { hasUI: host?.hasUI === true, ui };
|
|
2377
|
+
};
|
|
2378
|
+
const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
|
|
2186
2379
|
const loaded = await run.store.load();
|
|
2380
|
+
const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
|
|
2381
|
+
if (loaded.run.activeShells !== undefined) {
|
|
2382
|
+
await persistRunState(run.store, run.metadata, (current) => {
|
|
2383
|
+
const next = { ...current };
|
|
2384
|
+
delete next.activeShells;
|
|
2385
|
+
return next;
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2187
2388
|
await run.store.validateRetrySource();
|
|
2188
2389
|
await run.store.validateBorrowedWorktrees();
|
|
2189
2390
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
|
|
@@ -2206,7 +2407,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2206
2407
|
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
2408
|
if (!script)
|
|
2208
2409
|
throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
2209
|
-
|
|
2410
|
+
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
2411
|
+
await run.store.saveSnapshot(persistedSnapshot);
|
|
2210
2412
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
2211
2413
|
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) });
|
|
2212
2414
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
@@ -2222,7 +2424,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2222
2424
|
const typed = asWorkflowError(error);
|
|
2223
2425
|
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
|
|
2224
2426
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
2225
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current
|
|
2427
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed));
|
|
2226
2428
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
2227
2429
|
run.update?.(workflowToolUpdate(persisted));
|
|
2228
2430
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
@@ -2233,7 +2435,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2233
2435
|
}
|
|
2234
2436
|
await scheduler.cancelRun(run.store.runId);
|
|
2235
2437
|
await run.lifecycle.resume();
|
|
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,
|
|
2438
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
2237
2439
|
run.execution = execution;
|
|
2238
2440
|
const completion = execution.result.then(async (value) => {
|
|
2239
2441
|
await scheduler.flush();
|
|
@@ -2248,19 +2450,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2248
2450
|
const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
|
|
2249
2451
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2250
2452
|
await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2251
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot()
|
|
2453
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
|
|
2252
2454
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2253
2455
|
if (state === "failed")
|
|
2254
2456
|
retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
|
|
2255
2457
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2256
2458
|
run.update?.(workflowToolUpdate(persisted));
|
|
2257
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2258
|
-
await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted)
|
|
2459
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) {
|
|
2460
|
+
const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
|
|
2461
|
+
Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
|
|
2462
|
+
}
|
|
2463
|
+
throw typed;
|
|
2259
2464
|
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
2260
2465
|
run.completion = completion;
|
|
2261
|
-
|
|
2466
|
+
if (!foreground || !waitForCompletion) {
|
|
2467
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2468
|
+
deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
|
|
2469
|
+
}, (error) => {
|
|
2470
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2471
|
+
if (diagnostic)
|
|
2472
|
+
deliverFailure(pi, diagnostic);
|
|
2473
|
+
else
|
|
2474
|
+
deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
|
|
2475
|
+
});
|
|
2476
|
+
return undefined;
|
|
2477
|
+
}
|
|
2478
|
+
return completion;
|
|
2262
2479
|
};
|
|
2263
|
-
const applyBudgetDecision = async (request, approved, context, signal) => {
|
|
2480
|
+
const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
|
|
2264
2481
|
const run = runs.get(request.runId);
|
|
2265
2482
|
if (!run)
|
|
2266
2483
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
@@ -2274,10 +2491,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2274
2491
|
next.budget = nextBudget;
|
|
2275
2492
|
else
|
|
2276
2493
|
delete next.budget; return next; });
|
|
2277
|
-
|
|
2494
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2495
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
|
|
2496
|
+
if (completed)
|
|
2497
|
+
return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
|
|
2278
2498
|
return { state: "running", approved: true };
|
|
2279
2499
|
};
|
|
2280
|
-
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
2500
|
+
const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true) => {
|
|
2281
2501
|
const run = runs.get(runId);
|
|
2282
2502
|
if (!run) {
|
|
2283
2503
|
const host = object(context) ? context : {};
|
|
@@ -2323,11 +2543,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2323
2543
|
else
|
|
2324
2544
|
delete next.budget; return next; });
|
|
2325
2545
|
}
|
|
2326
|
-
|
|
2546
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2547
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
|
|
2548
|
+
if (completed)
|
|
2549
|
+
return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run };
|
|
2327
2550
|
return { state: "running" };
|
|
2328
2551
|
};
|
|
2329
2552
|
const retryReservations = new Set();
|
|
2330
|
-
const retryWorkflowRun = async (runId, context, signal) => {
|
|
2553
|
+
const retryWorkflowRun = async (runId, context, signal, modeOverride) => {
|
|
2331
2554
|
if (typeof runId !== "string" || !runId.trim())
|
|
2332
2555
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
2333
2556
|
const host = object(context) ? context : {};
|
|
@@ -2399,7 +2622,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2399
2622
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
2400
2623
|
const childInitialBudget = childBudget.snapshot();
|
|
2401
2624
|
const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
2402
|
-
await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [],
|
|
2625
|
+
await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
|
|
2403
2626
|
const fallbackModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
2404
2627
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
|
|
2405
2628
|
const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
|
|
@@ -2410,12 +2633,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2410
2633
|
runs.set(childRunId, childRun);
|
|
2411
2634
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2412
2635
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
2413
|
-
|
|
2636
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2637
|
+
const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
2414
2638
|
const completion = runs.get(childRunId)?.completion;
|
|
2415
2639
|
if (completion) {
|
|
2416
2640
|
childStarted = true;
|
|
2417
2641
|
void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
|
|
2418
2642
|
}
|
|
2643
|
+
else if (completed) {
|
|
2644
|
+
childStarted = true;
|
|
2645
|
+
retryReservations.delete(lineageRootRunId);
|
|
2646
|
+
}
|
|
2647
|
+
if (completed)
|
|
2648
|
+
return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
|
|
2419
2649
|
return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
|
|
2420
2650
|
}
|
|
2421
2651
|
finally {
|
|
@@ -2430,7 +2660,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2430
2660
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
2431
2661
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2432
2662
|
try {
|
|
2433
|
-
const result = await retryWorkflowRun(params.runId, ctx, signal);
|
|
2663
|
+
const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground);
|
|
2434
2664
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2435
2665
|
}
|
|
2436
2666
|
catch (error) {
|
|
@@ -2444,10 +2674,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2444
2674
|
name: "workflow_resume",
|
|
2445
2675
|
label: "Workflow Resume",
|
|
2446
2676
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
2447
|
-
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
2677
|
+
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
|
|
2448
2678
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2449
2679
|
try {
|
|
2450
|
-
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
|
|
2680
|
+
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground);
|
|
2451
2681
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2452
2682
|
}
|
|
2453
2683
|
catch (error) {
|
|
@@ -2484,11 +2714,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2484
2714
|
}
|
|
2485
2715
|
if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
|
|
2486
2716
|
const previousState = loaded.run.state;
|
|
2487
|
-
await store.updateState((current) =>
|
|
2717
|
+
await store.updateState((current) => {
|
|
2718
|
+
if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state))
|
|
2719
|
+
return current;
|
|
2720
|
+
const next = { ...current, state: "interrupted" };
|
|
2721
|
+
delete next.activeShells;
|
|
2722
|
+
return next;
|
|
2723
|
+
});
|
|
2488
2724
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2489
2725
|
await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
|
|
2490
2726
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2491
2727
|
}
|
|
2728
|
+
else if (loaded.run.activeShells !== undefined) {
|
|
2729
|
+
await store.updateState((current) => {
|
|
2730
|
+
if (["completed", "failed", "stopped"].includes(current.state))
|
|
2731
|
+
return current;
|
|
2732
|
+
const next = { ...current };
|
|
2733
|
+
delete next.activeShells;
|
|
2734
|
+
return next;
|
|
2735
|
+
});
|
|
2736
|
+
loaded = { ...loaded, run: (await store.load()).run };
|
|
2737
|
+
}
|
|
2492
2738
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
|
|
2493
2739
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
2494
2740
|
eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
|
|
@@ -2516,7 +2762,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2516
2762
|
const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
|
|
2517
2763
|
for (const run of toResume) {
|
|
2518
2764
|
try {
|
|
2519
|
-
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
|
|
2765
|
+
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2520
2766
|
ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
|
|
2521
2767
|
}
|
|
2522
2768
|
catch (err) {
|
|
@@ -2588,10 +2834,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2588
2834
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2589
2835
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
2590
2836
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
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 });
|
|
2837
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2838
|
+
let persistedSnapshot = snapshot;
|
|
2839
|
+
const captureFunctionRole = functionName ? async (role, model) => {
|
|
2840
|
+
const definition = agentDefinitions[role];
|
|
2841
|
+
if (!definition)
|
|
2842
|
+
return;
|
|
2843
|
+
const modelName = `${model.provider}/${model.model}`;
|
|
2844
|
+
const hasProjectRole = projectAgentDefinitions[role] !== undefined;
|
|
2845
|
+
if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName))
|
|
2846
|
+
return;
|
|
2847
|
+
const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
|
|
2848
|
+
const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
|
|
2849
|
+
const models = [...new Set([...persistedSnapshot.models, modelName])];
|
|
2850
|
+
persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
|
|
2851
|
+
await store.saveSnapshot(persistedSnapshot);
|
|
2852
|
+
} : undefined;
|
|
2592
2853
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2593
2854
|
const initialBudget = budgetRuntime.snapshot();
|
|
2594
|
-
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [],
|
|
2855
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2856
|
+
if (params.foreground)
|
|
2857
|
+
foregroundDeliveries.set(toolCallId, { store, inline: false });
|
|
2595
2858
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2596
2859
|
const background = !params.foreground;
|
|
2597
2860
|
const providerPause = async () => { if (background)
|
|
@@ -2602,7 +2865,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2602
2865
|
if (params.foreground && onUpdate)
|
|
2603
2866
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2604
2867
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
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);
|
|
2868
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, captureFunctionRole), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2606
2869
|
runs.get(runId).execution = execution;
|
|
2607
2870
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2608
2871
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2618,7 +2881,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2618
2881
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2619
2882
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
|
|
2620
2883
|
await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2621
|
-
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot()
|
|
2884
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
|
|
2622
2885
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2623
2886
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
2624
2887
|
const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
@@ -2629,18 +2892,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2629
2892
|
});
|
|
2630
2893
|
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
2631
2894
|
runs.get(runId).completion = completion;
|
|
2895
|
+
const deliverFailureContent = (error) => {
|
|
2896
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2897
|
+
return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
|
|
2898
|
+
};
|
|
2899
|
+
const queueForegroundDelivery = async (content) => {
|
|
2900
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
2901
|
+
if (!delivery)
|
|
2902
|
+
return;
|
|
2903
|
+
await store.updateState((current) => {
|
|
2904
|
+
if (!current.delivery || current.delivery.state === "delivered")
|
|
2905
|
+
return current;
|
|
2906
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
|
|
2907
|
+
});
|
|
2908
|
+
if (delivery.inline)
|
|
2909
|
+
return;
|
|
2910
|
+
scheduleForegroundDelivery(toolCallId, async () => {
|
|
2911
|
+
if (delivery.inline)
|
|
2912
|
+
return;
|
|
2913
|
+
pendingFailureDiagnostics.delete(toolCallId);
|
|
2914
|
+
await deliverTerminal(store, content);
|
|
2915
|
+
});
|
|
2916
|
+
};
|
|
2632
2917
|
if (background) {
|
|
2633
2918
|
void completion.then(async ({ value, resultPath }) => {
|
|
2634
|
-
|
|
2635
|
-
}, (error) => {
|
|
2636
|
-
|
|
2637
|
-
if (diagnostic)
|
|
2638
|
-
deliverFailure(pi, diagnostic);
|
|
2639
|
-
else
|
|
2640
|
-
deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
2919
|
+
await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2920
|
+
}, async (error) => {
|
|
2921
|
+
await deliverTerminal(store, deliverFailureContent(error));
|
|
2641
2922
|
});
|
|
2642
2923
|
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2643
2924
|
}
|
|
2925
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2926
|
+
await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2927
|
+
}, async (error) => {
|
|
2928
|
+
await queueForegroundDelivery(deliverFailureContent(error));
|
|
2929
|
+
});
|
|
2644
2930
|
const { value } = await completion;
|
|
2645
2931
|
const run = (await store.load()).run;
|
|
2646
2932
|
return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
@@ -2666,8 +2952,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2666
2952
|
clearInterval(state.workflowSpinner);
|
|
2667
2953
|
delete state.workflowSpinner;
|
|
2668
2954
|
}
|
|
2669
|
-
if (runDetails?.run)
|
|
2670
|
-
|
|
2955
|
+
if (runDetails?.run) {
|
|
2956
|
+
const incoming = runDetails.run;
|
|
2957
|
+
let progress = state.workflowProgress;
|
|
2958
|
+
if (!isPartial || !progress || progress.runId !== incoming.id) {
|
|
2959
|
+
progress = undefined;
|
|
2960
|
+
delete state.workflowProgress;
|
|
2961
|
+
if (isPartial) {
|
|
2962
|
+
progress = { runId: incoming.id, inputRun: incoming, run: incoming, lastRefreshAt: 0, runtimeStartedAt: Date.now(), runtimeBaseMs: incoming.usage?.durationMs ?? 0 };
|
|
2963
|
+
state.workflowProgress = progress;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
else if (progress.inputRun !== incoming) {
|
|
2967
|
+
if (progress.run.state !== "running" && incoming.state === "running") {
|
|
2968
|
+
progress.runtimeBaseMs = incoming.usage?.durationMs ?? 0;
|
|
2969
|
+
progress.runtimeStartedAt = Date.now();
|
|
2970
|
+
}
|
|
2971
|
+
progress.inputRun = incoming;
|
|
2972
|
+
progress.run = incoming;
|
|
2973
|
+
}
|
|
2974
|
+
return workflowProgressBlock(progress?.run ?? incoming, theme, progress, async () => {
|
|
2975
|
+
const active = runs.get(incoming.id);
|
|
2976
|
+
const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
|
|
2977
|
+
const loaded = await store.load();
|
|
2978
|
+
return withLiveActivities(loaded.run);
|
|
2979
|
+
}, () => { if (state.workflowProgress === progress)
|
|
2980
|
+
context.invalidate(); });
|
|
2981
|
+
}
|
|
2671
2982
|
const content = result.content[0];
|
|
2672
2983
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
2673
2984
|
},
|
|
@@ -2687,7 +2998,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2687
2998
|
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
2688
2999
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
2689
3000
|
try {
|
|
2690
|
-
|
|
3001
|
+
const loaded = await store.load();
|
|
3002
|
+
return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } };
|
|
2691
3003
|
}
|
|
2692
3004
|
catch {
|
|
2693
3005
|
if (!await store.isComplete())
|
|
@@ -2715,7 +3027,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2715
3027
|
return keepContext ? "dashboard" : "done";
|
|
2716
3028
|
}
|
|
2717
3029
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2718
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
3030
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
2719
3031
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2720
3032
|
return keepContext ? "dashboard" : "done";
|
|
2721
3033
|
}
|
|
@@ -2740,12 +3052,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2740
3052
|
if (action === "resume" && run) {
|
|
2741
3053
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2742
3054
|
const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
|
|
2743
|
-
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2744
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
3055
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
3056
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2745
3057
|
}
|
|
2746
3058
|
else {
|
|
2747
3059
|
if (run.lifecycle.state === "interrupted")
|
|
2748
|
-
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
3060
|
+
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2749
3061
|
else {
|
|
2750
3062
|
if (run.lifecycle.state === "paused")
|
|
2751
3063
|
await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
@@ -2759,8 +3071,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2759
3071
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
2760
3072
|
if (input === undefined)
|
|
2761
3073
|
return keepContext ? "dashboard" : "done";
|
|
2762
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2763
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
3074
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
3075
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2764
3076
|
return keepContext ? "dashboard" : "done";
|
|
2765
3077
|
}
|
|
2766
3078
|
if (action === "stop" && run) {
|
|
@@ -2981,11 +3293,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2981
3293
|
};
|
|
2982
3294
|
const loadDashboard = async () => {
|
|
2983
3295
|
const loaded = await store.load();
|
|
3296
|
+
const liveRun = withLiveActivities(loaded.run);
|
|
2984
3297
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2985
3298
|
const worktrees = await store.worktrees();
|
|
2986
3299
|
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
2987
3300
|
const agentResults = new Map();
|
|
2988
|
-
for (const agent of
|
|
3301
|
+
for (const agent of liveRun.agents) {
|
|
2989
3302
|
if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
|
|
2990
3303
|
continue;
|
|
2991
3304
|
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
@@ -2997,11 +3310,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2997
3310
|
const reviews = new Map();
|
|
2998
3311
|
const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
|
|
2999
3312
|
const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
3000
|
-
if (
|
|
3313
|
+
if (liveRun.state === "running")
|
|
3001
3314
|
add("Pause", "pause");
|
|
3002
|
-
if (["paused", "interrupted"].includes(
|
|
3315
|
+
if (["paused", "interrupted"].includes(liveRun.state))
|
|
3003
3316
|
add("Resume", "resume");
|
|
3004
|
-
if (
|
|
3317
|
+
if (liveRun.state === "budget_exhausted") {
|
|
3005
3318
|
actions.set("Resume unchanged", `resume ${store.runId}`);
|
|
3006
3319
|
actions.set("Adjust budget", `adjust ${store.runId}`);
|
|
3007
3320
|
}
|
|
@@ -3010,7 +3323,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3010
3323
|
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
3011
3324
|
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
3012
3325
|
}
|
|
3013
|
-
if (!terminalStates.has(
|
|
3326
|
+
if (!terminalStates.has(liveRun.state))
|
|
3014
3327
|
add("Stop", "stop");
|
|
3015
3328
|
for (const cp of checkpoints) {
|
|
3016
3329
|
if (ctx.mode === "tui") {
|
|
@@ -3027,47 +3340,50 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3027
3340
|
actions.set("Refresh", "refresh");
|
|
3028
3341
|
else
|
|
3029
3342
|
actions.set("Open script in editor", "open-script");
|
|
3030
|
-
if (ctx.mode !== "tui" &&
|
|
3343
|
+
if (ctx.mode !== "tui" && liveRun.agents.length)
|
|
3031
3344
|
actions.set("Agents...", "agents");
|
|
3032
|
-
if (terminalStates.has(
|
|
3345
|
+
if (terminalStates.has(liveRun.state))
|
|
3033
3346
|
add("Delete", "delete");
|
|
3034
3347
|
if (ctx.mode === "tui") {
|
|
3035
3348
|
addCopy("Copy run path", store.directory, "run path");
|
|
3036
3349
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3037
3350
|
}
|
|
3038
|
-
return { dashboard: formatWorkflowPhaseDashboard(
|
|
3351
|
+
return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
|
|
3039
3352
|
};
|
|
3040
3353
|
const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
3354
|
+
const agentAttemptActionContext = (dashboard, agent) => {
|
|
3355
|
+
const attempt = (agent.attemptDetails ?? []).reduce((latest, candidate) => !latest || candidate.attempt > latest.attempt ? candidate : latest, undefined);
|
|
3356
|
+
if (!attempt)
|
|
3357
|
+
return undefined;
|
|
3358
|
+
const liveCandidate = liveAgentSessions.get(`${dashboard.run.id}:${agent.id}`);
|
|
3359
|
+
const live = liveCandidate && attempt.session && liveCandidate.reference.transport === attempt.session.transport && liveCandidate.reference.sessionId === attempt.session.sessionId ? liveCandidate : undefined;
|
|
3360
|
+
const run = runs.get(dashboard.run.id);
|
|
3361
|
+
const ui = { notify: (message, level = "info") => { ctx.ui.notify(message, level); }, confirm: (title, message) => ctx.ui.confirm(title, message), select: (title, options) => { return ctx.ui.select(title, [...options]); }, input: (title, placeholder) => ctx.ui.input(title, placeholder) };
|
|
3362
|
+
const attemptSnapshot = deepFreeze(structuredClone(attempt));
|
|
3363
|
+
return { run: deepFreeze(structuredClone(dashboard.run)), agent: deepFreeze(structuredClone(agent)), attempt: attemptSnapshot, ...(attemptSnapshot.session ? { session: attemptSnapshot.session } : {}), ...(live ? { liveSession: live } : {}), signal: run?.abortController.signal ?? new AbortController().signal, ui: Object.freeze(ui) };
|
|
3364
|
+
};
|
|
3365
|
+
const visibleAgentAttemptActions = (dashboard, agent) => {
|
|
3366
|
+
const context = agentAttemptActionContext(dashboard, agent);
|
|
3367
|
+
if (!context)
|
|
3368
|
+
return [];
|
|
3369
|
+
return Object.entries(registry.agentAttemptActions()).filter(([, action]) => { try {
|
|
3370
|
+
return action.visible(context);
|
|
3371
|
+
}
|
|
3372
|
+
catch {
|
|
3373
|
+
return false;
|
|
3374
|
+
} });
|
|
3375
|
+
};
|
|
3041
3376
|
const agentActionLabels = (dashboard, agent) => {
|
|
3042
3377
|
const worktree = agentWorktreeFor(dashboard, agent);
|
|
3043
3378
|
return [
|
|
3044
|
-
...(agent.
|
|
3379
|
+
...visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
|
|
3045
3380
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3381
|
+
...(ctx.mode === "tui" && agent.prompt !== undefined ? ["Open prompt in editor"] : []),
|
|
3046
3382
|
...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
|
|
3047
3383
|
"Copy agent ID",
|
|
3048
3384
|
"Back",
|
|
3049
3385
|
];
|
|
3050
3386
|
};
|
|
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
3387
|
const selectAgent = async (dashboard, requestedAgentId) => {
|
|
3072
3388
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
3073
3389
|
const title = (agent) => agentBreadcrumb(agent, byId, true);
|
|
@@ -3088,6 +3404,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3088
3404
|
const action = await ctx.ui.select(title(selected), actions);
|
|
3089
3405
|
if (!action || action === "Back")
|
|
3090
3406
|
return;
|
|
3407
|
+
const extensionAction = visibleAgentAttemptActions(dashboard, selected).find(([, candidate]) => candidate.label === action);
|
|
3408
|
+
if (extensionAction) {
|
|
3409
|
+
const context = agentAttemptActionContext(dashboard, selected);
|
|
3410
|
+
if (context) {
|
|
3411
|
+
try {
|
|
3412
|
+
await extensionAction[1].run(context);
|
|
3413
|
+
}
|
|
3414
|
+
catch (error) {
|
|
3415
|
+
ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3091
3420
|
if (action === "Copy agent ID") {
|
|
3092
3421
|
await copyArtifact(selected.id, "agent ID");
|
|
3093
3422
|
continue;
|
|
@@ -3100,8 +3429,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3100
3429
|
await copyArtifact(worktree.path, "worktree path");
|
|
3101
3430
|
continue;
|
|
3102
3431
|
}
|
|
3103
|
-
if (action === "Fork as Pi session in pane")
|
|
3104
|
-
await forkAgentSession(dashboard, selected);
|
|
3105
3432
|
}
|
|
3106
3433
|
};
|
|
3107
3434
|
for (;;) {
|
|
@@ -3123,12 +3450,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3123
3450
|
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3124
3451
|
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
3125
3452
|
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
3126
|
-
const terminalRows = () => Math.max(1, tuiRows(tui) -
|
|
3453
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
3127
3454
|
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3128
|
-
const keyLabel = (binding, fallback) =>
|
|
3129
|
-
const keys = keybindingKeys(keybindings, binding);
|
|
3130
|
-
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
3131
|
-
};
|
|
3455
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3132
3456
|
const selectedAgentRecord = () => {
|
|
3133
3457
|
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3134
3458
|
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
@@ -3159,7 +3483,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3159
3483
|
}
|
|
3160
3484
|
finally {
|
|
3161
3485
|
editorRunning = false;
|
|
3162
|
-
tui.requestRender(true);
|
|
3163
3486
|
}
|
|
3164
3487
|
};
|
|
3165
3488
|
const updateDashboard = async () => {
|
|
@@ -3213,7 +3536,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3213
3536
|
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
3214
3537
|
}, 1000);
|
|
3215
3538
|
timer.unref();
|
|
3216
|
-
return
|
|
3539
|
+
return {
|
|
3217
3540
|
render(width) {
|
|
3218
3541
|
renderedWidth = width;
|
|
3219
3542
|
const narrow = width < 80;
|
|
@@ -3249,7 +3572,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3249
3572
|
selectionNeedsScroll = false;
|
|
3250
3573
|
}
|
|
3251
3574
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
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" : "
|
|
3575
|
+
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
3253
3576
|
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
3254
3577
|
},
|
|
3255
3578
|
invalidate() { },
|
|
@@ -3266,21 +3589,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3266
3589
|
}
|
|
3267
3590
|
if (actionMode) {
|
|
3268
3591
|
const options = actionOptions();
|
|
3269
|
-
if (keybindings
|
|
3592
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
3270
3593
|
actionMode = false;
|
|
3271
3594
|
dashboardOffset = 0;
|
|
3272
3595
|
tui.requestRender();
|
|
3273
3596
|
return;
|
|
3274
3597
|
}
|
|
3275
|
-
if (keybindings
|
|
3598
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3276
3599
|
actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
3277
|
-
else if (keybindings
|
|
3600
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3278
3601
|
actionIndex = (actionIndex + 1) % options.length;
|
|
3279
|
-
else if (keybindings
|
|
3602
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3280
3603
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3281
|
-
else if (keybindings
|
|
3604
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3282
3605
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3283
|
-
else if (keybindings
|
|
3606
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3284
3607
|
const action = options[actionIndex];
|
|
3285
3608
|
const agent = selectedAgentRecord();
|
|
3286
3609
|
if (!action || action === "Back") {
|
|
@@ -3289,7 +3612,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3289
3612
|
}
|
|
3290
3613
|
else if (agent) {
|
|
3291
3614
|
const worktree = agentWorktreeFor(view, agent);
|
|
3292
|
-
if (action === "Open
|
|
3615
|
+
if (action === "Open prompt in editor") {
|
|
3616
|
+
if (agent.prompt !== undefined)
|
|
3617
|
+
void openArtifact(Promise.resolve(workflowPromptArtifact(agent.prompt)), "agent prompt");
|
|
3618
|
+
}
|
|
3619
|
+
else if (action === "Open result in editor") {
|
|
3293
3620
|
const result = view.agentResults.get(agent.id);
|
|
3294
3621
|
if (result !== undefined)
|
|
3295
3622
|
void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
|
|
@@ -3300,8 +3627,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3300
3627
|
void copyArtifact(worktree.branch, "branch");
|
|
3301
3628
|
else if (action === "Copy worktree path" && worktree)
|
|
3302
3629
|
void copyArtifact(worktree.path, "worktree path");
|
|
3303
|
-
else
|
|
3304
|
-
|
|
3630
|
+
else {
|
|
3631
|
+
const extensionAction = visibleAgentAttemptActions(view, agent).find(([, candidate]) => candidate.label === action);
|
|
3632
|
+
const actionContext = extensionAction ? agentAttemptActionContext(view, agent) : undefined;
|
|
3633
|
+
if (extensionAction && actionContext) {
|
|
3634
|
+
actionMode = false;
|
|
3635
|
+
void Promise.resolve(extensionAction[1].run(actionContext)).catch((error) => { ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error"); }).finally(() => { void updateDashboard(); });
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3305
3638
|
}
|
|
3306
3639
|
else if (action === "Open script in editor")
|
|
3307
3640
|
void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
|
|
@@ -3314,20 +3647,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3314
3647
|
return;
|
|
3315
3648
|
}
|
|
3316
3649
|
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3317
|
-
if (keybindings
|
|
3650
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3318
3651
|
if (narrow && detailsMode) {
|
|
3319
3652
|
detailsMode = false;
|
|
3320
3653
|
selectionNeedsScroll = true;
|
|
3321
3654
|
}
|
|
3322
3655
|
else
|
|
3323
|
-
done(
|
|
3656
|
+
done("Back");
|
|
3324
3657
|
}
|
|
3325
3658
|
else if (narrow && detailsMode) {
|
|
3326
|
-
if (keybindings
|
|
3659
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3327
3660
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3328
|
-
else if (keybindings
|
|
3661
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3329
3662
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3330
|
-
else if (keybindings
|
|
3663
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3331
3664
|
if (current?.kind === "agent" && current.agentId) {
|
|
3332
3665
|
actionMode = true;
|
|
3333
3666
|
actionIndex = 0;
|
|
@@ -3340,33 +3673,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3340
3673
|
}
|
|
3341
3674
|
}
|
|
3342
3675
|
}
|
|
3343
|
-
else if (keybindings
|
|
3676
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
3344
3677
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
3345
3678
|
selectedNodeId = next.nodeId;
|
|
3346
3679
|
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3347
3680
|
selectionNeedsScroll = true;
|
|
3348
3681
|
}
|
|
3349
|
-
else if (keybindings
|
|
3682
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
3350
3683
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
3351
3684
|
selectedNodeId = next.nodeId;
|
|
3352
3685
|
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3353
3686
|
selectionNeedsScroll = true;
|
|
3354
3687
|
}
|
|
3355
|
-
else if (keybindings
|
|
3688
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
3356
3689
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
3357
3690
|
selectedNodeId = next.nodeId;
|
|
3358
3691
|
selectionNeedsScroll = true;
|
|
3359
3692
|
}
|
|
3360
|
-
else if (keybindings
|
|
3693
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
3361
3694
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
3362
3695
|
selectedNodeId = next.nodeId;
|
|
3363
3696
|
selectionNeedsScroll = true;
|
|
3364
3697
|
}
|
|
3365
|
-
else if (keybindings
|
|
3698
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3366
3699
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3367
|
-
else if (keybindings
|
|
3700
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3368
3701
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3369
|
-
else if (keybindings
|
|
3702
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3370
3703
|
if (narrow)
|
|
3371
3704
|
detailsMode = true;
|
|
3372
3705
|
else if (current?.kind === "agent" && current.agentId) {
|
|
@@ -3383,11 +3716,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3383
3716
|
tui.requestRender();
|
|
3384
3717
|
},
|
|
3385
3718
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "
|
|
3389
|
-
if (!actionChoice || actionChoice === "
|
|
3390
|
-
|
|
3719
|
+
};
|
|
3720
|
+
})
|
|
3721
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
3722
|
+
if (!actionChoice || actionChoice === "Back") {
|
|
3723
|
+
stores = await loadStores();
|
|
3724
|
+
break;
|
|
3725
|
+
}
|
|
3391
3726
|
if (actionChoice === "Agents...") {
|
|
3392
3727
|
await selectAgent(view);
|
|
3393
3728
|
continue;
|
|
@@ -3396,13 +3731,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3396
3731
|
await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
|
|
3397
3732
|
continue;
|
|
3398
3733
|
}
|
|
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);
|
|
3404
|
-
continue;
|
|
3405
|
-
}
|
|
3406
3734
|
if (actionChoice === "Refresh")
|
|
3407
3735
|
continue;
|
|
3408
3736
|
const copy = view.copies.get(actionChoice);
|
|
@@ -3439,7 +3767,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3439
3767
|
const currentLayout = layout();
|
|
3440
3768
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
3441
3769
|
offset = Math.min(offset, maxOffset);
|
|
3442
|
-
const
|
|
3770
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3771
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3772
|
+
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
3443
3773
|
const controls = currentLayout.compactControls
|
|
3444
3774
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
3445
3775
|
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
@@ -3453,17 +3783,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3453
3783
|
},
|
|
3454
3784
|
invalidate() { },
|
|
3455
3785
|
handleInput(data) {
|
|
3456
|
-
if (keybindings
|
|
3786
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3457
3787
|
selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
3458
|
-
else if (keybindings
|
|
3788
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3459
3789
|
selectedIndex = (selectedIndex + 1) % options.length;
|
|
3460
|
-
else if (keybindings
|
|
3790
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3461
3791
|
move(-layout().contentViewport);
|
|
3462
|
-
else if (keybindings
|
|
3792
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3463
3793
|
move(layout().contentViewport);
|
|
3464
|
-
else if (keybindings
|
|
3794
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
|
|
3465
3795
|
done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
|
|
3466
|
-
else if (keybindings
|
|
3796
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
|
|
3467
3797
|
done(undefined);
|
|
3468
3798
|
tui.requestRender();
|
|
3469
3799
|
},
|