pi-extensible-workflows 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -30
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/host.d.ts +9 -6
- package/dist/src/host.js +375 -129
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +8 -2
- 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 +2 -2
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/host.ts +329 -124
- package/src/index.ts +3 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +6 -2
- package/src/workflow-evals.ts +24 -3
package/dist/src/host.js
CHANGED
|
@@ -16,7 +16,7 @@ import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentR
|
|
|
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
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";
|
|
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;
|
|
@@ -170,7 +172,7 @@ export function formatWorkflowPreview(args) {
|
|
|
170
172
|
}
|
|
171
173
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
172
174
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
173
|
-
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Foreground results include the completed run ID. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId }) replays a failed run into a child; workflow_resume({ runId, budget? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
175
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
174
176
|
function workflowRecoveryGuidance(action, state) {
|
|
175
177
|
if (action === "resume") {
|
|
176
178
|
if (state === "failed")
|
|
@@ -204,7 +206,7 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
204
206
|
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
205
207
|
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
206
208
|
});
|
|
207
|
-
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
209
|
+
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
208
210
|
function phaseNames(source) {
|
|
209
211
|
const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
|
|
210
212
|
return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
@@ -547,7 +549,7 @@ function styleForState(map, state, styles) {
|
|
|
547
549
|
function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
548
550
|
function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
549
551
|
function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
550
|
-
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
552
|
+
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
551
553
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
552
554
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
553
555
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
@@ -555,11 +557,14 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
|
|
|
555
557
|
const lines = [`${iconStyle(workflowIcon)} ${header}`];
|
|
556
558
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
557
559
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
560
|
+
const activeShells = run.activeShells ?? 0;
|
|
561
|
+
if (activeShells > 0)
|
|
562
|
+
lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
|
|
558
563
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
559
564
|
const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
560
565
|
const icon = agentStateGlyph(agent.state, spinner);
|
|
561
566
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
562
|
-
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
567
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
563
568
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
564
569
|
const state = progressStyleForState(agent.state, styles);
|
|
565
570
|
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
|
|
@@ -623,7 +628,7 @@ function controlState(state, theme) {
|
|
|
623
628
|
return theme.fg(color, state);
|
|
624
629
|
}
|
|
625
630
|
function controlAction(action, theme) {
|
|
626
|
-
const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
631
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
627
632
|
return theme.fg(color, action);
|
|
628
633
|
}
|
|
629
634
|
function budgetPatchEntries(value) {
|
|
@@ -681,14 +686,15 @@ function workflowControlResult(name, args, result, expanded, theme, isError) {
|
|
|
681
686
|
if (name === "workflow_retry") {
|
|
682
687
|
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
683
688
|
const state = controlString(value.state) ?? "unknown";
|
|
689
|
+
const action = state === "completed" ? "completed" : "started";
|
|
684
690
|
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");
|
|
691
|
+
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
692
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
|
|
687
693
|
}
|
|
688
694
|
if (name === "workflow_resume") {
|
|
689
695
|
const state = controlString(value.state) ?? "unknown";
|
|
690
696
|
const proposalId = controlString(value.proposalId);
|
|
691
|
-
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
|
|
697
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
692
698
|
if (!expanded)
|
|
693
699
|
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
694
700
|
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal: ${theme.fg("accent", proposalId)}`] : []), ...budgetPatchDetails(args.budget, theme)].join("\n");
|
|
@@ -888,15 +894,41 @@ function styledAgentBreadcrumb(agent, byId, styles) {
|
|
|
888
894
|
return parts[0] ?? "";
|
|
889
895
|
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
890
896
|
}
|
|
891
|
-
function
|
|
897
|
+
export function formatStalledDuration(durationMs) {
|
|
898
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
899
|
+
if (minutes < 60)
|
|
900
|
+
return `${String(minutes)}m`;
|
|
901
|
+
const hours = Math.floor(minutes / 60);
|
|
902
|
+
const remainingMinutes = minutes % 60;
|
|
903
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
904
|
+
}
|
|
905
|
+
function stalledDuration(agent, now) {
|
|
906
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
|
|
907
|
+
return undefined;
|
|
908
|
+
const duration = now - agent.lastEventAt;
|
|
909
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
910
|
+
}
|
|
911
|
+
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
892
912
|
const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
|
|
893
|
-
|
|
913
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
914
|
+
const stalled = stalledDuration(agent, now);
|
|
915
|
+
if (stalled === undefined)
|
|
916
|
+
return activity;
|
|
917
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
918
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
919
|
+
}
|
|
920
|
+
function formatAccountingValue(value) {
|
|
921
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
894
922
|
}
|
|
895
923
|
function formatAccounting(accounting) {
|
|
896
924
|
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
897
|
-
return `${
|
|
925
|
+
return `${formatAccountingValue(total)} tok`;
|
|
926
|
+
}
|
|
927
|
+
function formatAgentAccounting(accounting) {
|
|
928
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
929
|
+
return [`Tokens: ${formatAccountingValue(total)} (in=${formatAccountingValue(accounting.input)} out=${formatAccountingValue(accounting.output)} cache-read=${formatAccountingValue(accounting.cacheRead)} cache-write=${formatAccountingValue(accounting.cacheWrite)})`, `Cost: $${accounting.cost.toFixed(2)}`];
|
|
898
930
|
}
|
|
899
|
-
export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
931
|
+
export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
|
|
900
932
|
void worktrees;
|
|
901
933
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
902
934
|
const totalAccounting = run.agents.reduce((sum, a) => ({ input: sum.input + (a.accounting?.input ?? 0), output: sum.output + (a.accounting?.output ?? 0), cacheRead: sum.cacheRead + (a.accounting?.cacheRead ?? 0), cacheWrite: sum.cacheWrite + (a.accounting?.cacheWrite ?? 0), cost: sum.cost + (a.accounting?.cost ?? 0) }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
@@ -922,7 +954,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
922
954
|
if (last?.error)
|
|
923
955
|
result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
924
956
|
}
|
|
925
|
-
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
|
|
957
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
926
958
|
if (activity)
|
|
927
959
|
result.push(`${indent} ${activity}`);
|
|
928
960
|
return result.join("\n");
|
|
@@ -935,7 +967,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
935
967
|
}
|
|
936
968
|
return lines.join("\n");
|
|
937
969
|
}
|
|
938
|
-
export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
970
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
|
|
939
971
|
const { run, snapshot } = loaded;
|
|
940
972
|
const lines = [
|
|
941
973
|
`Workflow: ${run.workflowName}`,
|
|
@@ -971,6 +1003,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
|
971
1003
|
result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
972
1004
|
for (const call of agent.toolCalls ?? [])
|
|
973
1005
|
result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
1006
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
1007
|
+
if (activity)
|
|
1008
|
+
result.push(`${indent} ${activity}`);
|
|
974
1009
|
return result.join("\n");
|
|
975
1010
|
}));
|
|
976
1011
|
lines.push("Checkpoints:");
|
|
@@ -982,7 +1017,7 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
|
|
|
982
1017
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
983
1018
|
return lines.join("\n");
|
|
984
1019
|
}
|
|
985
|
-
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
1020
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
986
1021
|
const safeWidth = Math.max(1, width);
|
|
987
1022
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
988
1023
|
const tree = buildWorkflowPhaseTree(model);
|
|
@@ -1022,7 +1057,8 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
1022
1057
|
const treeLine = (node) => {
|
|
1023
1058
|
const selected = node.id === selectedNode?.id;
|
|
1024
1059
|
const state = progressStyleForState(node.state, styles);
|
|
1025
|
-
|
|
1060
|
+
const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
|
|
1061
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
|
|
1026
1062
|
};
|
|
1027
1063
|
const details = (node) => {
|
|
1028
1064
|
if (!node)
|
|
@@ -1041,12 +1077,15 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
1041
1077
|
if (!agent)
|
|
1042
1078
|
return [styles.muted("Agent details are unavailable")];
|
|
1043
1079
|
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")])];
|
|
1080
|
+
const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
1045
1081
|
const error = agent.attemptDetails?.at(-1)?.error;
|
|
1046
1082
|
if (error)
|
|
1047
1083
|
result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
1048
1084
|
if (agent.activity)
|
|
1049
1085
|
result.push(`Activity: ${agent.activity.text}`);
|
|
1086
|
+
const stalled = stalledDuration(agent, now);
|
|
1087
|
+
if (stalled !== undefined)
|
|
1088
|
+
result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
|
|
1050
1089
|
return result;
|
|
1051
1090
|
};
|
|
1052
1091
|
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
@@ -1288,15 +1327,35 @@ export function formatWorkflowFailureDiagnostics(diagnostic) {
|
|
|
1288
1327
|
const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
|
|
1289
1328
|
return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
|
|
1290
1329
|
}
|
|
1330
|
+
function deliveryPart(value, maxBytes) { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
|
|
1331
|
+
export function formatWorkflowFailureDelivery(diagnostic) {
|
|
1332
|
+
const name = deliveryPart(diagnostic.workflowName, 128);
|
|
1333
|
+
const runId = deliveryPart(diagnostic.runId, 128);
|
|
1334
|
+
const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
|
|
1335
|
+
const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
|
|
1336
|
+
const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
|
|
1337
|
+
const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
|
|
1338
|
+
const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
|
|
1339
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1340
|
+
}
|
|
1341
|
+
function formatWorkflowFailureDeliveryFallback(workflowName, runId, runDirectory, error) {
|
|
1342
|
+
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
1343
|
+
const failedPath = workflowFailedAt(error);
|
|
1344
|
+
const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
|
|
1345
|
+
const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
|
|
1346
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1347
|
+
}
|
|
1291
1348
|
function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
|
|
1292
1349
|
function isWorkflowFailureDiagnostics(value) {
|
|
1293
1350
|
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
1294
1351
|
}
|
|
1295
1352
|
function deliver(pi, content) {
|
|
1353
|
+
if (typeof pi.sendMessage !== "function")
|
|
1354
|
+
return;
|
|
1296
1355
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1297
1356
|
}
|
|
1298
1357
|
function deliverFailure(pi, diagnostic) {
|
|
1299
|
-
deliver(pi,
|
|
1358
|
+
deliver(pi, formatWorkflowFailureDelivery(diagnostic));
|
|
1300
1359
|
}
|
|
1301
1360
|
function safeEventError(error) {
|
|
1302
1361
|
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
@@ -1633,6 +1692,7 @@ function tuiRows(tui) {
|
|
|
1633
1692
|
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1634
1693
|
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1635
1694
|
}
|
|
1695
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
1636
1696
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1637
1697
|
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1638
1698
|
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
|
|
@@ -1649,7 +1709,15 @@ function keybindingKeys(keybindings, name) {
|
|
|
1649
1709
|
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
|
|
1650
1710
|
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1651
1711
|
}
|
|
1652
|
-
|
|
1712
|
+
const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
1713
|
+
function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
1714
|
+
function workflowKeyLabel(keybindings, binding, fallback, labels) {
|
|
1715
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
1716
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
1717
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
1718
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
1719
|
+
}
|
|
1720
|
+
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir, additionalSkillPaths = []) {
|
|
1653
1721
|
beginWorkflowExtensionLoading();
|
|
1654
1722
|
const registry = loadingRegistry();
|
|
1655
1723
|
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
@@ -1702,16 +1770,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1702
1770
|
});
|
|
1703
1771
|
};
|
|
1704
1772
|
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
|
-
});
|
|
1773
|
+
const foregroundDeliveries = new Map();
|
|
1714
1774
|
const liveActivities = new Map();
|
|
1775
|
+
const liveEventTimes = new Map();
|
|
1715
1776
|
const setLiveActivity = (runId, agentId, activity) => {
|
|
1716
1777
|
const activities = liveActivities.get(runId);
|
|
1717
1778
|
if (activity) {
|
|
@@ -1726,12 +1787,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1726
1787
|
liveActivities.delete(runId);
|
|
1727
1788
|
}
|
|
1728
1789
|
};
|
|
1790
|
+
const setLiveEventTime = (runId, agentId, timestamp) => {
|
|
1791
|
+
if (timestamp === undefined)
|
|
1792
|
+
return;
|
|
1793
|
+
const timestamps = liveEventTimes.get(runId);
|
|
1794
|
+
if (timestamps)
|
|
1795
|
+
timestamps.set(agentId, timestamp);
|
|
1796
|
+
else
|
|
1797
|
+
liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
|
|
1798
|
+
};
|
|
1729
1799
|
const withLiveActivities = (run) => {
|
|
1730
1800
|
const activities = liveActivities.get(run.id);
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1801
|
+
const timestamps = liveEventTimes.get(run.id);
|
|
1802
|
+
if (!activities?.size && !timestamps?.size)
|
|
1803
|
+
return run;
|
|
1804
|
+
return { ...run, agents: run.agents.map((agent) => {
|
|
1805
|
+
const activity = activities?.get(agent.id);
|
|
1806
|
+
const lastEventAt = timestamps?.get(agent.id);
|
|
1807
|
+
if (activity === undefined && lastEventAt === undefined)
|
|
1808
|
+
return agent;
|
|
1809
|
+
return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1810
|
+
}) };
|
|
1735
1811
|
};
|
|
1736
1812
|
const terminalRunStates = new Map();
|
|
1737
1813
|
let sessionLease;
|
|
@@ -1759,6 +1835,52 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1759
1835
|
await eventPublisher.budget(store, metadata, persisted);
|
|
1760
1836
|
return persisted;
|
|
1761
1837
|
};
|
|
1838
|
+
pi.on("tool_result", async (event) => {
|
|
1839
|
+
const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
|
|
1840
|
+
if (delivery) {
|
|
1841
|
+
if (delivery.timer)
|
|
1842
|
+
clearTimeout(delivery.timer);
|
|
1843
|
+
delivery.inline = true;
|
|
1844
|
+
await delivery.store.updateState((current) => {
|
|
1845
|
+
if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered")
|
|
1846
|
+
return current;
|
|
1847
|
+
return { ...current, delivery: { ...current.delivery, state: "delivered" } };
|
|
1848
|
+
});
|
|
1849
|
+
foregroundDeliveries.delete(event.toolCallId);
|
|
1850
|
+
}
|
|
1851
|
+
if (event.toolName !== "workflow" || !event.isError)
|
|
1852
|
+
return;
|
|
1853
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1854
|
+
if (!diagnostic)
|
|
1855
|
+
return;
|
|
1856
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1857
|
+
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1858
|
+
});
|
|
1859
|
+
const deliverTerminal = async (store, content) => {
|
|
1860
|
+
let claimed;
|
|
1861
|
+
await store.updateState((current) => {
|
|
1862
|
+
if (current.delivery?.state === "delivered")
|
|
1863
|
+
return current;
|
|
1864
|
+
if (!current.delivery) {
|
|
1865
|
+
claimed = true;
|
|
1866
|
+
return current;
|
|
1867
|
+
}
|
|
1868
|
+
claimed = true;
|
|
1869
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
|
|
1870
|
+
});
|
|
1871
|
+
if (claimed === true)
|
|
1872
|
+
deliver(pi, content);
|
|
1873
|
+
};
|
|
1874
|
+
const scheduleForegroundDelivery = (toolCallId, send) => {
|
|
1875
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
1876
|
+
if (!delivery || delivery.inline || typeof pi.sendMessage !== "function")
|
|
1877
|
+
return;
|
|
1878
|
+
//NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
|
|
1879
|
+
delivery.timer = setTimeout(() => {
|
|
1880
|
+
delete delivery.timer;
|
|
1881
|
+
void send().finally(() => foregroundDeliveries.delete(toolCallId));
|
|
1882
|
+
}, 0);
|
|
1883
|
+
};
|
|
1762
1884
|
const phaseBridge = (store, metadata, lifecycle) => {
|
|
1763
1885
|
let cursor = 0;
|
|
1764
1886
|
return async (phase) => {
|
|
@@ -1811,10 +1933,25 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1811
1933
|
const replayed = await store.replay(path);
|
|
1812
1934
|
if (replayed)
|
|
1813
1935
|
return readShellResult(replayed.value);
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1936
|
+
const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
|
|
1937
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
|
|
1938
|
+
try {
|
|
1939
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
1940
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
1941
|
+
await store.complete(path, result);
|
|
1942
|
+
return result;
|
|
1943
|
+
}
|
|
1944
|
+
finally {
|
|
1945
|
+
const stopped = await persistRunState(store, metadata, (current) => {
|
|
1946
|
+
const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
|
|
1947
|
+
if (activeShells > 0)
|
|
1948
|
+
return { ...current, activeShells };
|
|
1949
|
+
const next = { ...current };
|
|
1950
|
+
delete next.activeShells;
|
|
1951
|
+
return next;
|
|
1952
|
+
});
|
|
1953
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
|
|
1954
|
+
}
|
|
1818
1955
|
}
|
|
1819
1956
|
finally {
|
|
1820
1957
|
await lifecycle.leave();
|
|
@@ -1825,8 +1962,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1825
1962
|
budget.transition(next);
|
|
1826
1963
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
1827
1964
|
const nextRun = { ...current, state: next, ...budget.snapshot() };
|
|
1828
|
-
if (next === "running" || next === "completed")
|
|
1965
|
+
if (next === "running" || next === "completed") {
|
|
1829
1966
|
delete nextRun.error;
|
|
1967
|
+
delete nextRun.failedAt;
|
|
1968
|
+
}
|
|
1830
1969
|
return nextRun;
|
|
1831
1970
|
});
|
|
1832
1971
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
@@ -1841,28 +1980,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1841
1980
|
const onProgress = async (progress) => {
|
|
1842
1981
|
let runState;
|
|
1843
1982
|
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);
|
|
1983
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
|
|
1845
1984
|
}
|
|
1846
1985
|
else {
|
|
1847
1986
|
const loaded = await run.store.load();
|
|
1848
1987
|
if (!loaded.run.agents.some((agent) => agent.id === id))
|
|
1849
1988
|
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) };
|
|
1989
|
+
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
|
|
1851
1990
|
}
|
|
1852
1991
|
if (!runState.agents.some((agent) => agent.id === id))
|
|
1853
1992
|
return;
|
|
1854
1993
|
setLiveActivity(runId, id, progress.activity);
|
|
1994
|
+
setLiveEventTime(runId, id, progress.lastEventAt);
|
|
1855
1995
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
1856
1996
|
};
|
|
1857
1997
|
const onAttempt = async (attempt) => {
|
|
1858
1998
|
await scheduler.flush();
|
|
1859
1999
|
scheduler.attemptStarted(id);
|
|
2000
|
+
const lastEventAt = Date.now();
|
|
2001
|
+
setLiveEventTime(runId, id, lastEventAt);
|
|
1860
2002
|
await scheduler.flush();
|
|
1861
2003
|
const before = (await run.store.load()).run;
|
|
1862
2004
|
await persistActiveAgentAttempt(run.store, id, attempt);
|
|
1863
2005
|
const active = (await run.store.load()).run;
|
|
1864
2006
|
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() }));
|
|
2007
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
1866
2008
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1867
2009
|
};
|
|
1868
2010
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
@@ -1908,7 +2050,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1908
2050
|
effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : { ...run.model, ...(node.options.thinking ? { thinking: node.options.thinking } : {}) }, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools };
|
|
1909
2051
|
}
|
|
1910
2052
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
1911
|
-
|
|
2053
|
+
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
|
|
2054
|
+
return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1912
2055
|
});
|
|
1913
2056
|
return { ...current, agents };
|
|
1914
2057
|
});
|
|
@@ -1927,6 +2070,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1927
2070
|
terminalRunStates.set(runId, run.lifecycle.state);
|
|
1928
2071
|
run.checkpointResolvers.clear();
|
|
1929
2072
|
liveActivities.delete(runId);
|
|
2073
|
+
liveEventTimes.delete(runId);
|
|
1930
2074
|
eventPublisher.removeRun(runId);
|
|
1931
2075
|
runs.delete(runId);
|
|
1932
2076
|
};
|
|
@@ -1967,7 +2111,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1967
2111
|
run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
|
|
1968
2112
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1969
2113
|
};
|
|
1970
|
-
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
|
|
2114
|
+
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
|
|
1971
2115
|
const run = runs.get(runId);
|
|
1972
2116
|
if (!run)
|
|
1973
2117
|
return undefined;
|
|
@@ -1975,7 +2119,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1975
2119
|
if (!request)
|
|
1976
2120
|
return undefined;
|
|
1977
2121
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1978
|
-
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
2122
|
+
const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
|
|
1979
2123
|
if (!silent)
|
|
1980
2124
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1981
2125
|
return result;
|
|
@@ -2105,7 +2249,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2105
2249
|
});
|
|
2106
2250
|
catalogRegistered = true;
|
|
2107
2251
|
};
|
|
2108
|
-
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
2252
|
+
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
2109
2253
|
const activeSnapshotTools = (tools, active) => active === "session"
|
|
2110
2254
|
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
2111
2255
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
@@ -2135,7 +2279,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2135
2279
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
2136
2280
|
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
2137
2281
|
};
|
|
2138
|
-
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => async (prompt, options, agentSignal, identity) => {
|
|
2282
|
+
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId, captureRole) => async (prompt, options, agentSignal, identity) => {
|
|
2139
2283
|
await lifecycle.enter();
|
|
2140
2284
|
try {
|
|
2141
2285
|
const path = agentIdentityPath(identity);
|
|
@@ -2150,6 +2294,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2150
2294
|
const thinking = parseThinking(options.thinking);
|
|
2151
2295
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
2152
2296
|
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
|
|
2297
|
+
if (role)
|
|
2298
|
+
await captureRole?.(role, resolved.model);
|
|
2153
2299
|
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
2154
2300
|
const tools = resolved.tools;
|
|
2155
2301
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
@@ -2182,8 +2328,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2182
2328
|
if (drift.length)
|
|
2183
2329
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2184
2330
|
};
|
|
2185
|
-
const
|
|
2331
|
+
const recoveryUi = (context) => {
|
|
2332
|
+
const host = object(context) ? context : undefined;
|
|
2333
|
+
const ui = host && object(host.ui) ? host.ui : {};
|
|
2334
|
+
return { hasUI: host?.hasUI === true, ui };
|
|
2335
|
+
};
|
|
2336
|
+
const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
|
|
2186
2337
|
const loaded = await run.store.load();
|
|
2338
|
+
const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
|
|
2339
|
+
if (loaded.run.activeShells !== undefined) {
|
|
2340
|
+
await persistRunState(run.store, run.metadata, (current) => {
|
|
2341
|
+
const next = { ...current };
|
|
2342
|
+
delete next.activeShells;
|
|
2343
|
+
return next;
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2187
2346
|
await run.store.validateRetrySource();
|
|
2188
2347
|
await run.store.validateBorrowedWorktrees();
|
|
2189
2348
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
|
|
@@ -2206,7 +2365,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2206
2365
|
const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
|
|
2207
2366
|
if (!script)
|
|
2208
2367
|
throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
2209
|
-
|
|
2368
|
+
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
2369
|
+
await run.store.saveSnapshot(persistedSnapshot);
|
|
2210
2370
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
2211
2371
|
run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
|
|
2212
2372
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
@@ -2222,7 +2382,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2222
2382
|
const typed = asWorkflowError(error);
|
|
2223
2383
|
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
|
|
2224
2384
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
2225
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current
|
|
2385
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed));
|
|
2226
2386
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
2227
2387
|
run.update?.(workflowToolUpdate(persisted));
|
|
2228
2388
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
@@ -2233,7 +2393,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2233
2393
|
}
|
|
2234
2394
|
await scheduler.cancelRun(run.store.runId);
|
|
2235
2395
|
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,
|
|
2396
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
2237
2397
|
run.execution = execution;
|
|
2238
2398
|
const completion = execution.result.then(async (value) => {
|
|
2239
2399
|
await scheduler.flush();
|
|
@@ -2248,19 +2408,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2248
2408
|
const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
|
|
2249
2409
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2250
2410
|
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()
|
|
2411
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
|
|
2252
2412
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2253
2413
|
if (state === "failed")
|
|
2254
2414
|
retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
|
|
2255
2415
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2256
2416
|
run.update?.(workflowToolUpdate(persisted));
|
|
2257
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2258
|
-
await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted)
|
|
2417
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) {
|
|
2418
|
+
const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
|
|
2419
|
+
Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
|
|
2420
|
+
}
|
|
2421
|
+
throw typed;
|
|
2259
2422
|
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
2260
2423
|
run.completion = completion;
|
|
2261
|
-
|
|
2424
|
+
if (!foreground || !waitForCompletion) {
|
|
2425
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2426
|
+
deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
|
|
2427
|
+
}, (error) => {
|
|
2428
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2429
|
+
if (diagnostic)
|
|
2430
|
+
deliverFailure(pi, diagnostic);
|
|
2431
|
+
else
|
|
2432
|
+
deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
|
|
2433
|
+
});
|
|
2434
|
+
return undefined;
|
|
2435
|
+
}
|
|
2436
|
+
return completion;
|
|
2262
2437
|
};
|
|
2263
|
-
const applyBudgetDecision = async (request, approved, context, signal) => {
|
|
2438
|
+
const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
|
|
2264
2439
|
const run = runs.get(request.runId);
|
|
2265
2440
|
if (!run)
|
|
2266
2441
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
@@ -2274,10 +2449,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2274
2449
|
next.budget = nextBudget;
|
|
2275
2450
|
else
|
|
2276
2451
|
delete next.budget; return next; });
|
|
2277
|
-
|
|
2452
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2453
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
|
|
2454
|
+
if (completed)
|
|
2455
|
+
return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
|
|
2278
2456
|
return { state: "running", approved: true };
|
|
2279
2457
|
};
|
|
2280
|
-
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
2458
|
+
const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true) => {
|
|
2281
2459
|
const run = runs.get(runId);
|
|
2282
2460
|
if (!run) {
|
|
2283
2461
|
const host = object(context) ? context : {};
|
|
@@ -2323,11 +2501,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2323
2501
|
else
|
|
2324
2502
|
delete next.budget; return next; });
|
|
2325
2503
|
}
|
|
2326
|
-
|
|
2504
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2505
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
|
|
2506
|
+
if (completed)
|
|
2507
|
+
return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run };
|
|
2327
2508
|
return { state: "running" };
|
|
2328
2509
|
};
|
|
2329
2510
|
const retryReservations = new Set();
|
|
2330
|
-
const retryWorkflowRun = async (runId, context, signal) => {
|
|
2511
|
+
const retryWorkflowRun = async (runId, context, signal, modeOverride) => {
|
|
2331
2512
|
if (typeof runId !== "string" || !runId.trim())
|
|
2332
2513
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
2333
2514
|
const host = object(context) ? context : {};
|
|
@@ -2410,12 +2591,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2410
2591
|
runs.set(childRunId, childRun);
|
|
2411
2592
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2412
2593
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
2413
|
-
|
|
2594
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2595
|
+
const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
2414
2596
|
const completion = runs.get(childRunId)?.completion;
|
|
2415
2597
|
if (completion) {
|
|
2416
2598
|
childStarted = true;
|
|
2417
2599
|
void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
|
|
2418
2600
|
}
|
|
2601
|
+
else if (completed) {
|
|
2602
|
+
childStarted = true;
|
|
2603
|
+
retryReservations.delete(lineageRootRunId);
|
|
2604
|
+
}
|
|
2605
|
+
if (completed)
|
|
2606
|
+
return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
|
|
2419
2607
|
return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
|
|
2420
2608
|
}
|
|
2421
2609
|
finally {
|
|
@@ -2430,7 +2618,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2430
2618
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
2431
2619
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2432
2620
|
try {
|
|
2433
|
-
const result = await retryWorkflowRun(params.runId, ctx, signal);
|
|
2621
|
+
const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground);
|
|
2434
2622
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2435
2623
|
}
|
|
2436
2624
|
catch (error) {
|
|
@@ -2444,10 +2632,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2444
2632
|
name: "workflow_resume",
|
|
2445
2633
|
label: "Workflow Resume",
|
|
2446
2634
|
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 }),
|
|
2635
|
+
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
|
|
2448
2636
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2449
2637
|
try {
|
|
2450
|
-
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
|
|
2638
|
+
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground);
|
|
2451
2639
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2452
2640
|
}
|
|
2453
2641
|
catch (error) {
|
|
@@ -2484,11 +2672,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2484
2672
|
}
|
|
2485
2673
|
if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
|
|
2486
2674
|
const previousState = loaded.run.state;
|
|
2487
|
-
await store.updateState((current) =>
|
|
2675
|
+
await store.updateState((current) => {
|
|
2676
|
+
if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state))
|
|
2677
|
+
return current;
|
|
2678
|
+
const next = { ...current, state: "interrupted" };
|
|
2679
|
+
delete next.activeShells;
|
|
2680
|
+
return next;
|
|
2681
|
+
});
|
|
2488
2682
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2489
2683
|
await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
|
|
2490
2684
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2491
2685
|
}
|
|
2686
|
+
else if (loaded.run.activeShells !== undefined) {
|
|
2687
|
+
await store.updateState((current) => {
|
|
2688
|
+
if (["completed", "failed", "stopped"].includes(current.state))
|
|
2689
|
+
return current;
|
|
2690
|
+
const next = { ...current };
|
|
2691
|
+
delete next.activeShells;
|
|
2692
|
+
return next;
|
|
2693
|
+
});
|
|
2694
|
+
loaded = { ...loaded, run: (await store.load()).run };
|
|
2695
|
+
}
|
|
2492
2696
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
|
|
2493
2697
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
2494
2698
|
eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
|
|
@@ -2516,7 +2720,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2516
2720
|
const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
|
|
2517
2721
|
for (const run of toResume) {
|
|
2518
2722
|
try {
|
|
2519
|
-
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
|
|
2723
|
+
await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2520
2724
|
ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
|
|
2521
2725
|
}
|
|
2522
2726
|
catch (err) {
|
|
@@ -2588,10 +2792,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2588
2792
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2589
2793
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
2590
2794
|
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 });
|
|
2795
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2796
|
+
let persistedSnapshot = snapshot;
|
|
2797
|
+
const captureFunctionRole = functionName ? async (role, model) => {
|
|
2798
|
+
const definition = agentDefinitions[role];
|
|
2799
|
+
if (!definition)
|
|
2800
|
+
return;
|
|
2801
|
+
const modelName = `${model.provider}/${model.model}`;
|
|
2802
|
+
const hasProjectRole = projectAgentDefinitions[role] !== undefined;
|
|
2803
|
+
if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName))
|
|
2804
|
+
return;
|
|
2805
|
+
const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
|
|
2806
|
+
const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
|
|
2807
|
+
const models = [...new Set([...persistedSnapshot.models, modelName])];
|
|
2808
|
+
persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
|
|
2809
|
+
await store.saveSnapshot(persistedSnapshot);
|
|
2810
|
+
} : undefined;
|
|
2592
2811
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2593
2812
|
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: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2813
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2814
|
+
if (params.foreground)
|
|
2815
|
+
foregroundDeliveries.set(toolCallId, { store, inline: false });
|
|
2595
2816
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2596
2817
|
const background = !params.foreground;
|
|
2597
2818
|
const providerPause = async () => { if (background)
|
|
@@ -2602,7 +2823,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2602
2823
|
if (params.foreground && onUpdate)
|
|
2603
2824
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2604
2825
|
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);
|
|
2826
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, captureFunctionRole), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2606
2827
|
runs.get(runId).execution = execution;
|
|
2607
2828
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2608
2829
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2618,7 +2839,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2618
2839
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2619
2840
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
|
|
2620
2841
|
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()
|
|
2842
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
|
|
2622
2843
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2623
2844
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
2624
2845
|
const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
@@ -2629,18 +2850,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2629
2850
|
});
|
|
2630
2851
|
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
2631
2852
|
runs.get(runId).completion = completion;
|
|
2853
|
+
const deliverFailureContent = (error) => {
|
|
2854
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2855
|
+
return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
|
|
2856
|
+
};
|
|
2857
|
+
const queueForegroundDelivery = async (content) => {
|
|
2858
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
2859
|
+
if (!delivery)
|
|
2860
|
+
return;
|
|
2861
|
+
await store.updateState((current) => {
|
|
2862
|
+
if (!current.delivery || current.delivery.state === "delivered")
|
|
2863
|
+
return current;
|
|
2864
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
|
|
2865
|
+
});
|
|
2866
|
+
if (delivery.inline)
|
|
2867
|
+
return;
|
|
2868
|
+
scheduleForegroundDelivery(toolCallId, async () => {
|
|
2869
|
+
if (delivery.inline)
|
|
2870
|
+
return;
|
|
2871
|
+
pendingFailureDiagnostics.delete(toolCallId);
|
|
2872
|
+
await deliverTerminal(store, content);
|
|
2873
|
+
});
|
|
2874
|
+
};
|
|
2632
2875
|
if (background) {
|
|
2633
2876
|
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)}`);
|
|
2877
|
+
await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2878
|
+
}, async (error) => {
|
|
2879
|
+
await deliverTerminal(store, deliverFailureContent(error));
|
|
2641
2880
|
});
|
|
2642
2881
|
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2643
2882
|
}
|
|
2883
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2884
|
+
await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2885
|
+
}, async (error) => {
|
|
2886
|
+
await queueForegroundDelivery(deliverFailureContent(error));
|
|
2887
|
+
});
|
|
2644
2888
|
const { value } = await completion;
|
|
2645
2889
|
const run = (await store.load()).run;
|
|
2646
2890
|
return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
@@ -2687,7 +2931,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2687
2931
|
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
2688
2932
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
2689
2933
|
try {
|
|
2690
|
-
|
|
2934
|
+
const loaded = await store.load();
|
|
2935
|
+
return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } };
|
|
2691
2936
|
}
|
|
2692
2937
|
catch {
|
|
2693
2938
|
if (!await store.isComplete())
|
|
@@ -2715,7 +2960,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2715
2960
|
return keepContext ? "dashboard" : "done";
|
|
2716
2961
|
}
|
|
2717
2962
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2718
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2963
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
2719
2964
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2720
2965
|
return keepContext ? "dashboard" : "done";
|
|
2721
2966
|
}
|
|
@@ -2740,12 +2985,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2740
2985
|
if (action === "resume" && run) {
|
|
2741
2986
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2742
2987
|
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 === "
|
|
2988
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
2989
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2745
2990
|
}
|
|
2746
2991
|
else {
|
|
2747
2992
|
if (run.lifecycle.state === "interrupted")
|
|
2748
|
-
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2993
|
+
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2749
2994
|
else {
|
|
2750
2995
|
if (run.lifecycle.state === "paused")
|
|
2751
2996
|
await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
@@ -2759,8 +3004,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2759
3004
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
2760
3005
|
if (input === undefined)
|
|
2761
3006
|
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 === "
|
|
3007
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
3008
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2764
3009
|
return keepContext ? "dashboard" : "done";
|
|
2765
3010
|
}
|
|
2766
3011
|
if (action === "stop" && run) {
|
|
@@ -2981,11 +3226,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2981
3226
|
};
|
|
2982
3227
|
const loadDashboard = async () => {
|
|
2983
3228
|
const loaded = await store.load();
|
|
3229
|
+
const liveRun = withLiveActivities(loaded.run);
|
|
2984
3230
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2985
3231
|
const worktrees = await store.worktrees();
|
|
2986
3232
|
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
2987
3233
|
const agentResults = new Map();
|
|
2988
|
-
for (const agent of
|
|
3234
|
+
for (const agent of liveRun.agents) {
|
|
2989
3235
|
if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
|
|
2990
3236
|
continue;
|
|
2991
3237
|
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
@@ -2997,11 +3243,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2997
3243
|
const reviews = new Map();
|
|
2998
3244
|
const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
|
|
2999
3245
|
const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
3000
|
-
if (
|
|
3246
|
+
if (liveRun.state === "running")
|
|
3001
3247
|
add("Pause", "pause");
|
|
3002
|
-
if (["paused", "interrupted"].includes(
|
|
3248
|
+
if (["paused", "interrupted"].includes(liveRun.state))
|
|
3003
3249
|
add("Resume", "resume");
|
|
3004
|
-
if (
|
|
3250
|
+
if (liveRun.state === "budget_exhausted") {
|
|
3005
3251
|
actions.set("Resume unchanged", `resume ${store.runId}`);
|
|
3006
3252
|
actions.set("Adjust budget", `adjust ${store.runId}`);
|
|
3007
3253
|
}
|
|
@@ -3010,7 +3256,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3010
3256
|
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
3011
3257
|
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
3012
3258
|
}
|
|
3013
|
-
if (!terminalStates.has(
|
|
3259
|
+
if (!terminalStates.has(liveRun.state))
|
|
3014
3260
|
add("Stop", "stop");
|
|
3015
3261
|
for (const cp of checkpoints) {
|
|
3016
3262
|
if (ctx.mode === "tui") {
|
|
@@ -3027,15 +3273,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3027
3273
|
actions.set("Refresh", "refresh");
|
|
3028
3274
|
else
|
|
3029
3275
|
actions.set("Open script in editor", "open-script");
|
|
3030
|
-
if (ctx.mode !== "tui" &&
|
|
3276
|
+
if (ctx.mode !== "tui" && liveRun.agents.length)
|
|
3031
3277
|
actions.set("Agents...", "agents");
|
|
3032
|
-
if (terminalStates.has(
|
|
3278
|
+
if (terminalStates.has(liveRun.state))
|
|
3033
3279
|
add("Delete", "delete");
|
|
3034
3280
|
if (ctx.mode === "tui") {
|
|
3035
3281
|
addCopy("Copy run path", store.directory, "run path");
|
|
3036
3282
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3037
3283
|
}
|
|
3038
|
-
return { dashboard: formatWorkflowPhaseDashboard(
|
|
3284
|
+
return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
|
|
3039
3285
|
};
|
|
3040
3286
|
const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
3041
3287
|
const agentActionLabels = (dashboard, agent) => {
|
|
@@ -3123,12 +3369,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3123
3369
|
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
3124
3370
|
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
3125
3371
|
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
3126
|
-
const terminalRows = () => Math.max(1, tuiRows(tui) -
|
|
3372
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
3127
3373
|
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
|
-
};
|
|
3374
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3132
3375
|
const selectedAgentRecord = () => {
|
|
3133
3376
|
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3134
3377
|
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
@@ -3159,7 +3402,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3159
3402
|
}
|
|
3160
3403
|
finally {
|
|
3161
3404
|
editorRunning = false;
|
|
3162
|
-
tui.requestRender(true);
|
|
3163
3405
|
}
|
|
3164
3406
|
};
|
|
3165
3407
|
const updateDashboard = async () => {
|
|
@@ -3213,7 +3455,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3213
3455
|
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
3214
3456
|
}, 1000);
|
|
3215
3457
|
timer.unref();
|
|
3216
|
-
return
|
|
3458
|
+
return {
|
|
3217
3459
|
render(width) {
|
|
3218
3460
|
renderedWidth = width;
|
|
3219
3461
|
const narrow = width < 80;
|
|
@@ -3249,7 +3491,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3249
3491
|
selectionNeedsScroll = false;
|
|
3250
3492
|
}
|
|
3251
3493
|
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" : "
|
|
3494
|
+
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
3253
3495
|
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
3254
3496
|
},
|
|
3255
3497
|
invalidate() { },
|
|
@@ -3266,21 +3508,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3266
3508
|
}
|
|
3267
3509
|
if (actionMode) {
|
|
3268
3510
|
const options = actionOptions();
|
|
3269
|
-
if (keybindings
|
|
3511
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3270
3512
|
actionMode = false;
|
|
3271
3513
|
dashboardOffset = 0;
|
|
3272
3514
|
tui.requestRender();
|
|
3273
3515
|
return;
|
|
3274
3516
|
}
|
|
3275
|
-
if (keybindings
|
|
3517
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3276
3518
|
actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
3277
|
-
else if (keybindings
|
|
3519
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3278
3520
|
actionIndex = (actionIndex + 1) % options.length;
|
|
3279
|
-
else if (keybindings
|
|
3521
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3280
3522
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3281
|
-
else if (keybindings
|
|
3523
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3282
3524
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3283
|
-
else if (keybindings
|
|
3525
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3284
3526
|
const action = options[actionIndex];
|
|
3285
3527
|
const agent = selectedAgentRecord();
|
|
3286
3528
|
if (!action || action === "Back") {
|
|
@@ -3314,20 +3556,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3314
3556
|
return;
|
|
3315
3557
|
}
|
|
3316
3558
|
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
3317
|
-
if (keybindings
|
|
3559
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3318
3560
|
if (narrow && detailsMode) {
|
|
3319
3561
|
detailsMode = false;
|
|
3320
3562
|
selectionNeedsScroll = true;
|
|
3321
3563
|
}
|
|
3322
3564
|
else
|
|
3323
|
-
done(
|
|
3565
|
+
done("Back");
|
|
3324
3566
|
}
|
|
3325
3567
|
else if (narrow && detailsMode) {
|
|
3326
|
-
if (keybindings
|
|
3568
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3327
3569
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3328
|
-
else if (keybindings
|
|
3570
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3329
3571
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3330
|
-
else if (keybindings
|
|
3572
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3331
3573
|
if (current?.kind === "agent" && current.agentId) {
|
|
3332
3574
|
actionMode = true;
|
|
3333
3575
|
actionIndex = 0;
|
|
@@ -3340,33 +3582,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3340
3582
|
}
|
|
3341
3583
|
}
|
|
3342
3584
|
}
|
|
3343
|
-
else if (keybindings
|
|
3585
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
3344
3586
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
3345
3587
|
selectedNodeId = next.nodeId;
|
|
3346
3588
|
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3347
3589
|
selectionNeedsScroll = true;
|
|
3348
3590
|
}
|
|
3349
|
-
else if (keybindings
|
|
3591
|
+
else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
3350
3592
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
3351
3593
|
selectedNodeId = next.nodeId;
|
|
3352
3594
|
expandedNodeIds = new Set(next.expandedNodeIds);
|
|
3353
3595
|
selectionNeedsScroll = true;
|
|
3354
3596
|
}
|
|
3355
|
-
else if (keybindings
|
|
3597
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
3356
3598
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
3357
3599
|
selectedNodeId = next.nodeId;
|
|
3358
3600
|
selectionNeedsScroll = true;
|
|
3359
3601
|
}
|
|
3360
|
-
else if (keybindings
|
|
3602
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
3361
3603
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
3362
3604
|
selectedNodeId = next.nodeId;
|
|
3363
3605
|
selectionNeedsScroll = true;
|
|
3364
3606
|
}
|
|
3365
|
-
else if (keybindings
|
|
3607
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3366
3608
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3367
|
-
else if (keybindings
|
|
3609
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3368
3610
|
dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3369
|
-
else if (keybindings
|
|
3611
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
3370
3612
|
if (narrow)
|
|
3371
3613
|
detailsMode = true;
|
|
3372
3614
|
else if (current?.kind === "agent" && current.agentId) {
|
|
@@ -3383,11 +3625,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3383
3625
|
tui.requestRender();
|
|
3384
3626
|
},
|
|
3385
3627
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "
|
|
3389
|
-
if (!actionChoice || actionChoice === "
|
|
3390
|
-
|
|
3628
|
+
};
|
|
3629
|
+
})
|
|
3630
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
3631
|
+
if (!actionChoice || actionChoice === "Back") {
|
|
3632
|
+
stores = await loadStores();
|
|
3633
|
+
break;
|
|
3634
|
+
}
|
|
3391
3635
|
if (actionChoice === "Agents...") {
|
|
3392
3636
|
await selectAgent(view);
|
|
3393
3637
|
continue;
|
|
@@ -3439,7 +3683,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3439
3683
|
const currentLayout = layout();
|
|
3440
3684
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
3441
3685
|
offset = Math.min(offset, maxOffset);
|
|
3442
|
-
const
|
|
3686
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3687
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3688
|
+
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
3443
3689
|
const controls = currentLayout.compactControls
|
|
3444
3690
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
3445
3691
|
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
@@ -3453,17 +3699,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3453
3699
|
},
|
|
3454
3700
|
invalidate() { },
|
|
3455
3701
|
handleInput(data) {
|
|
3456
|
-
if (keybindings
|
|
3702
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up"))
|
|
3457
3703
|
selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
3458
|
-
else if (keybindings
|
|
3704
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
|
|
3459
3705
|
selectedIndex = (selectedIndex + 1) % options.length;
|
|
3460
|
-
else if (keybindings
|
|
3706
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
|
|
3461
3707
|
move(-layout().contentViewport);
|
|
3462
|
-
else if (keybindings
|
|
3708
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
|
|
3463
3709
|
move(layout().contentViewport);
|
|
3464
|
-
else if (keybindings
|
|
3710
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
|
|
3465
3711
|
done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
|
|
3466
|
-
else if (keybindings
|
|
3712
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
|
|
3467
3713
|
done(undefined);
|
|
3468
3714
|
tui.requestRender();
|
|
3469
3715
|
},
|