pi-extensible-workflows 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -0
- package/src/workflow-evals.ts +24 -3
package/src/host.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { Type } from "@earendil-works/pi-ai";
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import { FairAgentScheduler, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
11
11
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
12
12
|
import { acquireSessionLease, listRunIds, RunStore, SessionLease, structuralPath as operationPath } from "./persistence.js";
|
|
13
13
|
import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
@@ -16,8 +16,8 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
|
|
|
16
16
|
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
17
17
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
18
18
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
19
|
-
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
20
|
-
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, type AgentOptions, type AgentRecord, type AgentResourcePolicy, type BudgetApprovalRequest, type BudgetEvent, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowBridge, type WorkflowCatalogFunction, type WorkflowCatalogIndex, type WorkflowCatalogVariable, type WorkflowCheckpointState, type WorkflowErrorCode, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowFunctionContext, type WorkflowExecution, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowRetryProvenance, type WorkflowRunContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowSiblingAgent, type WorkflowWorktreeReference } from "./types.js";
|
|
19
|
+
import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
20
|
+
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, type AgentAttemptActionContext, type AgentAttemptSummary, type AgentOptions, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type BudgetApprovalRequest, type BudgetEvent, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowBridge, type WorkflowCatalogFunction, type WorkflowCatalogIndex, type WorkflowCatalogVariable, type WorkflowCheckpointState, type WorkflowErrorCode, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowFunctionContext, type WorkflowExecution, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowRetryProvenance, type WorkflowRunContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowSiblingAgent, type WorkflowWorktreeReference } from "./types.js";
|
|
21
21
|
const SETTLED_AGENT_STATES: ReadonlySet<import("./types.js").AgentState> = new Set(["completed", "failed", "cancelled"]);
|
|
22
22
|
const INTERNAL_WORKFLOW_TOOLS: readonly string[] = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
|
|
23
23
|
const HARD_TERMINAL_RUN_STATES: ReadonlySet<string> = new Set(["completed", "failed", "stopped"]);
|
|
@@ -96,6 +96,8 @@ function mainAgentError(error: unknown): WorkflowError {
|
|
|
96
96
|
Object.assign(presented, typed);
|
|
97
97
|
return presented;
|
|
98
98
|
}
|
|
99
|
+
function workflowFailedAt(error: unknown): string | undefined { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
|
|
100
|
+
function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
99
101
|
|
|
100
102
|
export class RunLifecycle {
|
|
101
103
|
#state: RunState;
|
|
@@ -163,13 +165,13 @@ export class RunLifecycle {
|
|
|
163
165
|
export function formatWorkflowPreview(args: { script?: unknown; workflow?: unknown; name?: unknown; description?: unknown }): string {
|
|
164
166
|
const explicitName = typeof args.name === "string" && args.name.trim() ? args.name.trim() : undefined;
|
|
165
167
|
const registeredName = typeof args.workflow === "string" && args.workflow.trim() ? args.workflow.trim() : undefined;
|
|
166
|
-
const name =
|
|
167
|
-
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ?
|
|
168
|
+
const name = explicitName ?? registeredName ?? "workflow";
|
|
169
|
+
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
|
|
168
170
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
169
171
|
}
|
|
170
172
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
171
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default"
|
|
172
|
-
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
|
|
173
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default"
|
|
174
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline and file-backed launches require a non-empty name; registered function launches may use name as an optional run label and otherwise use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes."
|
|
173
175
|
function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState): string {
|
|
174
176
|
if (action === "resume") {
|
|
175
177
|
if (state === "failed") return "Failed workflow runs must use workflow_retry({ runId })";
|
|
@@ -185,17 +187,18 @@ function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState):
|
|
|
185
187
|
return `Only failed workflow runs can be retried; source is ${state}`;
|
|
186
188
|
}
|
|
187
189
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
188
|
-
name: Type.Optional(Type.String({ description: "
|
|
190
|
+
name: Type.Optional(Type.String({ description: "Optional run label; required and non-empty for inline or file-backed launches, defaults to the registered function name when omitted" })),
|
|
189
191
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
190
192
|
script: Type.Optional(Type.String({ description: "Immutable inline workflow source; default to a named script that fans out with parallel(...) and awaits results before passing them to a summarizing agent(...)" })),
|
|
191
|
-
|
|
193
|
+
scriptPath: Type.Optional(Type.String({ description: "Path to a JavaScript workflow file, read once at launch and persisted as the inline source" })),
|
|
194
|
+
workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name; name may optionally label the run" })),
|
|
192
195
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
193
196
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
|
|
194
197
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
|
|
195
198
|
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
196
199
|
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
197
200
|
});
|
|
198
|
-
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
201
|
+
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" })) });
|
|
199
202
|
|
|
200
203
|
type WorkflowToolUpdate = { content: [{ type: "text"; text: string }]; details: { runId: string; run: PersistedRun } };
|
|
201
204
|
export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
|
|
@@ -482,19 +485,33 @@ function styleForState(map: Record<string, ProgressStyleKey>, state: string, sty
|
|
|
482
485
|
function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
483
486
|
function workflowIconStyle(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
484
487
|
function phaseStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
485
|
-
|
|
488
|
+
function formatWorkflowRuntime(durationMs: number): string {
|
|
489
|
+
const seconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
490
|
+
if (seconds < 60) return `${String(seconds)}s`;
|
|
491
|
+
const minutes = Math.floor(seconds / 60);
|
|
492
|
+
const remainingSeconds = seconds % 60;
|
|
493
|
+
if (minutes < 60) return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
|
|
494
|
+
const hours = Math.floor(minutes / 60);
|
|
495
|
+
const remainingMinutes = minutes % 60;
|
|
496
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
497
|
+
}
|
|
498
|
+
export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string {
|
|
486
499
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
487
500
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
488
501
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
489
502
|
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
490
|
-
const
|
|
503
|
+
const state = progressStyleForState(run.state, styles)(`[${run.state}]`);
|
|
504
|
+
const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
505
|
+
const lines = [`${iconStyle(workflowIcon)} ${header} ${state}${runtime}`];
|
|
491
506
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
492
507
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
508
|
+
const activeShells = run.activeShells ?? 0;
|
|
509
|
+
if (activeShells > 0) lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
|
|
493
510
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
494
511
|
const renderAgents = (agents: readonly AgentRecord[], offset: number, nested: boolean) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
495
512
|
const icon = agentStateGlyph(agent.state, spinner);
|
|
496
513
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
497
|
-
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
514
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
498
515
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
499
516
|
const state = progressStyleForState(agent.state, styles);
|
|
500
517
|
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
|
|
@@ -518,6 +535,7 @@ function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
|
|
|
518
535
|
}
|
|
519
536
|
|
|
520
537
|
const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
538
|
+
const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
|
|
521
539
|
|
|
522
540
|
function textBlock(text: string) {
|
|
523
541
|
return {
|
|
@@ -560,7 +578,7 @@ function controlState(state: string, theme: Theme): string {
|
|
|
560
578
|
return theme.fg(color, state);
|
|
561
579
|
}
|
|
562
580
|
function controlAction(action: string, theme: Theme): string {
|
|
563
|
-
const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
581
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
564
582
|
return theme.fg(color, action);
|
|
565
583
|
}
|
|
566
584
|
function budgetPatchEntries(value: unknown): string[] {
|
|
@@ -611,13 +629,14 @@ function workflowControlResult(name: string, args: Record<string, unknown>, resu
|
|
|
611
629
|
if (name === "workflow_retry") {
|
|
612
630
|
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
613
631
|
const state = controlString(value.state) ?? "unknown";
|
|
614
|
-
|
|
615
|
-
return [title, `Source
|
|
632
|
+
const action = state === "completed" ? "completed" : "started";
|
|
633
|
+
if (!expanded) return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
634
|
+
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");
|
|
616
635
|
}
|
|
617
636
|
if (name === "workflow_resume") {
|
|
618
637
|
const state = controlString(value.state) ?? "unknown";
|
|
619
638
|
const proposalId = controlString(value.proposalId);
|
|
620
|
-
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
|
|
639
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
621
640
|
if (!expanded) return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
622
641
|
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");
|
|
623
642
|
}
|
|
@@ -733,14 +752,38 @@ function themeWorkflowProgressStyles(theme: Theme): WorkflowProgressStyles {
|
|
|
733
752
|
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
734
753
|
};
|
|
735
754
|
}
|
|
736
|
-
|
|
755
|
+
type WorkflowProgressRefreshState = { runId: string; inputRun: PersistedRun; run: PersistedRun; lastRefreshAt: number; runtimeStartedAt: number; runtimeBaseMs: number; refresh?: Promise<void> };
|
|
756
|
+
function workflowProgressBlock(run: PersistedRun, theme: Theme, progress?: WorkflowProgressRefreshState, refresh?: () => Promise<PersistedRun | undefined>, invalidate?: () => void) {
|
|
737
757
|
const styles = themeWorkflowProgressStyles(theme);
|
|
758
|
+
const currentRun = () => {
|
|
759
|
+
const displayed = progress?.run ?? run;
|
|
760
|
+
if (!progress || displayed.state !== "running") return displayed;
|
|
761
|
+
const durationMs = Math.max(displayed.usage?.durationMs ?? 0, progress.runtimeBaseMs + Date.now() - progress.runtimeStartedAt);
|
|
762
|
+
return { ...displayed, usage: { ...budgetUsage(displayed.usage), durationMs } };
|
|
763
|
+
};
|
|
738
764
|
return {
|
|
739
765
|
render(width: number) {
|
|
740
766
|
const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
|
|
741
|
-
return truncateWorkflowProgress(formatWorkflowProgress(
|
|
767
|
+
return truncateWorkflowProgress(formatWorkflowProgress(currentRun(), frame, styles), width);
|
|
768
|
+
},
|
|
769
|
+
invalidate() {
|
|
770
|
+
const displayed = currentRun();
|
|
771
|
+
if (!progress || !refresh || displayed.state !== "running" || !displayed.agents.some((agent) => agent.state === "running")) return;
|
|
772
|
+
const now = Date.now();
|
|
773
|
+
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS) return;
|
|
774
|
+
progress.lastRefreshAt = now;
|
|
775
|
+
const inputRun = progress.inputRun;
|
|
776
|
+
const pending = refresh().then((next) => {
|
|
777
|
+
if (next && progress.inputRun === inputRun) {
|
|
778
|
+
progress.run = next;
|
|
779
|
+
invalidate?.();
|
|
780
|
+
}
|
|
781
|
+
}).catch(() => undefined);
|
|
782
|
+
progress.refresh = pending;
|
|
783
|
+
void pending.finally(() => {
|
|
784
|
+
if (progress.refresh === pending) delete progress.refresh;
|
|
785
|
+
});
|
|
742
786
|
},
|
|
743
|
-
invalidate() {},
|
|
744
787
|
};
|
|
745
788
|
}
|
|
746
789
|
export function formatBudgetStatus(run: Pick<PersistedRun, "budget" | "budgetVersion" | "usage" | "budgetEvents">): string[] {
|
|
@@ -809,18 +852,42 @@ function styledAgentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord
|
|
|
809
852
|
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
810
853
|
}
|
|
811
854
|
|
|
812
|
-
function
|
|
855
|
+
export function formatStalledDuration(durationMs: number): string {
|
|
856
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
857
|
+
if (minutes < 60) return `${String(minutes)}m`;
|
|
858
|
+
const hours = Math.floor(minutes / 60);
|
|
859
|
+
const remainingMinutes = minutes % 60;
|
|
860
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
861
|
+
}
|
|
862
|
+
function stalledDuration(agent: AgentRecord, now: number): number | undefined {
|
|
863
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt)) return undefined;
|
|
864
|
+
const duration = now - agent.lastEventAt;
|
|
865
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
866
|
+
}
|
|
867
|
+
function formatAgentActivity(agent: AgentRecord, spinner: string, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string {
|
|
813
868
|
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 ?? "";
|
|
814
|
-
|
|
869
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
870
|
+
const stalled = stalledDuration(agent, now);
|
|
871
|
+
if (stalled === undefined) return activity;
|
|
872
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
873
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function formatAccountingValue(value: number): string {
|
|
877
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
815
878
|
}
|
|
816
879
|
|
|
817
880
|
function formatAccounting(accounting: NonNullable<AgentRecord["accounting"]>): string {
|
|
818
881
|
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
819
|
-
return `${
|
|
882
|
+
return `${formatAccountingValue(total)} tok`;
|
|
820
883
|
}
|
|
821
884
|
|
|
885
|
+
function formatAgentAccounting(accounting: NonNullable<AgentRecord["accounting"]>): string[] {
|
|
886
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
887
|
+
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)}`];
|
|
888
|
+
}
|
|
822
889
|
|
|
823
|
-
export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string {
|
|
890
|
+
export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now = Date.now()): string {
|
|
824
891
|
void worktrees;
|
|
825
892
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
826
893
|
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 });
|
|
@@ -843,7 +910,7 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
843
910
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
844
911
|
if (last?.error) result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
845
912
|
}
|
|
846
|
-
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
|
|
913
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
847
914
|
if (activity) result.push(`${indent} ${activity}`);
|
|
848
915
|
return result.join("\n");
|
|
849
916
|
};
|
|
@@ -852,7 +919,7 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
852
919
|
return lines.join("\n");
|
|
853
920
|
}
|
|
854
921
|
|
|
855
|
-
export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string {
|
|
922
|
+
export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now = Date.now()): string {
|
|
856
923
|
const { run, snapshot } = loaded;
|
|
857
924
|
const lines = [
|
|
858
925
|
`Workflow: ${run.workflowName}`,
|
|
@@ -881,16 +948,18 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
881
948
|
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${role}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
|
|
882
949
|
for (const attempt of agent.attemptDetails ?? []) result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
883
950
|
for (const call of agent.toolCalls ?? []) result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
951
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
952
|
+
if (activity) result.push(`${indent} ${activity}`);
|
|
884
953
|
return result.join("\n");
|
|
885
954
|
}));
|
|
886
955
|
lines.push("Checkpoints:");
|
|
887
956
|
if (!checkpoints.length) lines.push(" (none)");
|
|
888
957
|
for (const checkpoint of checkpoints) lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
889
958
|
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
890
|
-
lines.push(`
|
|
959
|
+
lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
|
|
891
960
|
return lines.join("\n");
|
|
892
961
|
}
|
|
893
|
-
export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string[] {
|
|
962
|
+
export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string[] {
|
|
894
963
|
const safeWidth = Math.max(1, width);
|
|
895
964
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
896
965
|
const tree = buildWorkflowPhaseTree(model);
|
|
@@ -921,7 +990,8 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
921
990
|
const treeLine = (node: WorkflowPhaseTreeNode): string => {
|
|
922
991
|
const selected = node.id === selectedNode?.id;
|
|
923
992
|
const state = progressStyleForState(node.state, styles);
|
|
924
|
-
|
|
993
|
+
const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
|
|
994
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
|
|
925
995
|
};
|
|
926
996
|
const details = (node: WorkflowPhaseTreeNode | undefined): string[] => {
|
|
927
997
|
if (!node) return [styles.muted("No workflow node is selected")];
|
|
@@ -938,10 +1008,12 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
938
1008
|
const agent = node.agent;
|
|
939
1009
|
if (!agent) return [styles.muted("Agent details are unavailable")];
|
|
940
1010
|
const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
|
|
941
|
-
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")])];
|
|
1011
|
+
const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, ...(agent.systemPrompt === undefined ? [] : [`System prompt: ${agent.systemPrompt}`]), `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
942
1012
|
const error = agent.attemptDetails?.at(-1)?.error;
|
|
943
1013
|
if (error) result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
944
1014
|
if (agent.activity) result.push(`Activity: ${agent.activity.text}`);
|
|
1015
|
+
const stalled = stalledDuration(agent, now);
|
|
1016
|
+
if (stalled !== undefined) result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
|
|
945
1017
|
return result;
|
|
946
1018
|
};
|
|
947
1019
|
const stateNames: readonly WorkflowPhaseState[] = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
@@ -1018,8 +1090,8 @@ function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): W
|
|
|
1018
1090
|
...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
|
|
1019
1091
|
structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
1020
1092
|
attempt: value.failedAgent.attempt,
|
|
1021
|
-
...(value.failedAgent.
|
|
1022
|
-
...(value.failedAgent.
|
|
1093
|
+
...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
|
|
1094
|
+
...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
|
|
1023
1095
|
} } : {}),
|
|
1024
1096
|
completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
|
|
1025
1097
|
id: utf8Prefix(agent.id, 128),
|
|
@@ -1045,8 +1117,6 @@ function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): W
|
|
|
1045
1117
|
bounded = { ...bounded, retry };
|
|
1046
1118
|
continue;
|
|
1047
1119
|
}
|
|
1048
|
-
if (bounded.failedAgent?.sessionFile) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionFile; bounded = { ...bounded, failedAgent }; continue; }
|
|
1049
|
-
if (bounded.failedAgent?.sessionId) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionId; bounded = { ...bounded, failedAgent }; continue; }
|
|
1050
1120
|
if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } }; continue; }
|
|
1051
1121
|
if (Buffer.byteLength(bounded.error.message) > 256) { bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } }; continue; }
|
|
1052
1122
|
if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) { bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) }; continue; }
|
|
@@ -1086,8 +1156,8 @@ async function createWorkflowFailureDiagnostics(store: RunStore, metadata: Workf
|
|
|
1086
1156
|
...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
|
|
1087
1157
|
structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
|
|
1088
1158
|
attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
|
|
1089
|
-
...(failedAttempt?.
|
|
1090
|
-
...(failedAttempt?.
|
|
1159
|
+
...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
|
|
1160
|
+
...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
|
|
1091
1161
|
} satisfies WorkflowFailureAgent : undefined;
|
|
1092
1162
|
const completedSiblingAgents = run.agents.filter((agent) => {
|
|
1093
1163
|
if (agent.state !== "completed" || agent.id === failedAgentRecord?.id) return false;
|
|
@@ -1121,22 +1191,41 @@ async function createWorkflowFailureDiagnostics(store: RunStore, metadata: Workf
|
|
|
1121
1191
|
}
|
|
1122
1192
|
|
|
1123
1193
|
export function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string {
|
|
1124
|
-
const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.
|
|
1194
|
+
const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.session ? ` session=${diagnostic.failedAgent.session.transport}/${diagnostic.failedAgent.session.sessionId}` : ""}` : "(not persisted)";
|
|
1125
1195
|
const siblingAgents = diagnostic.completedSiblingAgents;
|
|
1126
1196
|
const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
|
|
1127
1197
|
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}`] : [];
|
|
1128
1198
|
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");
|
|
1129
1199
|
}
|
|
1200
|
+
function deliveryPart(value: string, maxBytes: number): string { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
|
|
1201
|
+
export function formatWorkflowFailureDelivery(diagnostic: WorkflowFailureDiagnostics): string {
|
|
1202
|
+
const name = deliveryPart(diagnostic.workflowName, 128);
|
|
1203
|
+
const runId = deliveryPart(diagnostic.runId, 128);
|
|
1204
|
+
const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
|
|
1205
|
+
const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
|
|
1206
|
+
const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
|
|
1207
|
+
const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
|
|
1208
|
+
const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
|
|
1209
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1210
|
+
}
|
|
1211
|
+
function formatWorkflowFailureDeliveryFallback(workflowName: string, runId: string, runDirectory: string, error: unknown): string {
|
|
1212
|
+
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
1213
|
+
const failedPath = workflowFailedAt(error);
|
|
1214
|
+
const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
|
|
1215
|
+
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)}`;
|
|
1216
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1217
|
+
}
|
|
1130
1218
|
|
|
1131
1219
|
function serializeWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string { return JSON.stringify(diagnostic); }
|
|
1132
1220
|
function isWorkflowFailureDiagnostics(value: unknown): value is WorkflowFailureDiagnostics {
|
|
1133
1221
|
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
1134
1222
|
}
|
|
1135
1223
|
function deliver(pi: ExtensionAPI, content: string): void {
|
|
1224
|
+
if (typeof pi.sendMessage !== "function") return;
|
|
1136
1225
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1137
1226
|
}
|
|
1138
1227
|
function deliverFailure(pi: ExtensionAPI, diagnostic: WorkflowFailureDiagnostics): void {
|
|
1139
|
-
deliver(pi,
|
|
1228
|
+
deliver(pi, formatWorkflowFailureDelivery(diagnostic));
|
|
1140
1229
|
}
|
|
1141
1230
|
|
|
1142
1231
|
type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -1456,6 +1545,7 @@ function tuiRows(tui: unknown): number {
|
|
|
1456
1545
|
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1457
1546
|
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1458
1547
|
}
|
|
1548
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
1459
1549
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1460
1550
|
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1461
1551
|
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } } as const;
|
|
@@ -1474,8 +1564,17 @@ function keybindingKeys(keybindings: unknown, name: string): readonly string[] |
|
|
|
1474
1564
|
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) as NonNullable<KeybindingsHostCapabilities["getKeys"]> | undefined : undefined;
|
|
1475
1565
|
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1476
1566
|
}
|
|
1567
|
+
type WorkflowKeybindings = { matches(data: string, binding: string): boolean };
|
|
1568
|
+
const WORKFLOW_VIM_KEYS: Readonly<Record<string, string>> = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
1569
|
+
function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
1570
|
+
function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels: Readonly<Record<string, string>>): string {
|
|
1571
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
1572
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
1573
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
1574
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
1575
|
+
}
|
|
1477
1576
|
|
|
1478
|
-
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard,
|
|
1577
|
+
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, transport: AgentTransport = localAgentTransport, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
|
|
1479
1578
|
beginWorkflowExtensionLoading();
|
|
1480
1579
|
const registry = loadingRegistry();
|
|
1481
1580
|
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
@@ -1496,7 +1595,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1496
1595
|
const skillPath = [join(extensionDir, "../skills"), join(extensionDir, "../../skills")].find((path) => existsSync(path));
|
|
1497
1596
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
1498
1597
|
});
|
|
1499
|
-
type BudgetDecisionResult = { state: "running" | "budget_exhausted"; approved: boolean };
|
|
1598
|
+
type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun };
|
|
1500
1599
|
const runs = new Map<string, { executor: WorkflowAgentExecutor; store: RunStore; metadata: WorkflowMetadata; model: ModelSpec; lifecycle: RunLifecycle; budget: WorkflowBudgetRuntime; abortController: AbortController; projectTrusted: () => boolean; providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>; execution?: WorkflowExecution; completion?: Promise<unknown>; checkpointResolvers: Map<string, (value: boolean) => void>; update?: (result: WorkflowToolUpdate) => void }>();
|
|
1501
1600
|
let providerRecoveryQueue = Promise.resolve();
|
|
1502
1601
|
const enqueueProviderRecovery = <T>(task: () => Promise<T>): Promise<T> => { const next = providerRecoveryQueue.then(task, task); providerRecoveryQueue = next.then(() => undefined, () => undefined); return next; };
|
|
@@ -1520,14 +1619,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1520
1619
|
});
|
|
1521
1620
|
};
|
|
1522
1621
|
const pendingFailureDiagnostics = new Map<string, WorkflowFailureDiagnostics>();
|
|
1523
|
-
|
|
1524
|
-
if (event.toolName !== "workflow" || !event.isError) return;
|
|
1525
|
-
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1526
|
-
if (!diagnostic) return;
|
|
1527
|
-
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1528
|
-
return { content: [{ type: "text" as const, text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1529
|
-
});
|
|
1622
|
+
const foregroundDeliveries = new Map<string, { store: RunStore; inline: boolean; timer?: ReturnType<typeof setTimeout> }>();
|
|
1530
1623
|
const liveActivities = new Map<string, Map<string, AgentActivity>>();
|
|
1624
|
+
const liveEventTimes = new Map<string, Map<string, number>>();
|
|
1625
|
+
const liveAgentSessions = new Map<string, import("./types.js").WorkflowAgentSession>();
|
|
1626
|
+
const setLiveAgentSession = (runId: string, agentId: string, session?: import("./types.js").WorkflowAgentSession) => { const key = `${runId}:${agentId}`; if (session) liveAgentSessions.set(key, session); else liveAgentSessions.delete(key); };
|
|
1531
1627
|
const setLiveActivity = (runId: string, agentId: string, activity?: AgentActivity) => {
|
|
1532
1628
|
const activities = liveActivities.get(runId);
|
|
1533
1629
|
if (activity) {
|
|
@@ -1538,12 +1634,22 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1538
1634
|
if (activities?.size === 0) liveActivities.delete(runId);
|
|
1539
1635
|
}
|
|
1540
1636
|
};
|
|
1637
|
+
const setLiveEventTime = (runId: string, agentId: string, timestamp?: number) => {
|
|
1638
|
+
if (timestamp === undefined) return;
|
|
1639
|
+
const timestamps = liveEventTimes.get(runId);
|
|
1640
|
+
if (timestamps) timestamps.set(agentId, timestamp);
|
|
1641
|
+
else liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
|
|
1642
|
+
};
|
|
1541
1643
|
const withLiveActivities = (run: PersistedRun): PersistedRun => {
|
|
1542
1644
|
const activities = liveActivities.get(run.id);
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1645
|
+
const timestamps = liveEventTimes.get(run.id);
|
|
1646
|
+
if (!activities?.size && !timestamps?.size) return run;
|
|
1647
|
+
return { ...run, agents: run.agents.map((agent) => {
|
|
1648
|
+
const activity = activities?.get(agent.id);
|
|
1649
|
+
const lastEventAt = timestamps?.get(agent.id);
|
|
1650
|
+
if (activity === undefined && lastEventAt === undefined) return agent;
|
|
1651
|
+
return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1652
|
+
}) };
|
|
1547
1653
|
};
|
|
1548
1654
|
const terminalRunStates = new Map<string, "completed" | "failed" | "stopped">();
|
|
1549
1655
|
let sessionLease: SessionLease | undefined;
|
|
@@ -1565,6 +1671,42 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1565
1671
|
await eventPublisher.budget(store, metadata, persisted);
|
|
1566
1672
|
return persisted;
|
|
1567
1673
|
};
|
|
1674
|
+
pi.on("tool_result", async (event) => {
|
|
1675
|
+
const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
|
|
1676
|
+
if (delivery) {
|
|
1677
|
+
if (delivery.timer) clearTimeout(delivery.timer);
|
|
1678
|
+
delivery.inline = true;
|
|
1679
|
+
await delivery.store.updateState((current) => {
|
|
1680
|
+
if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered") return current;
|
|
1681
|
+
return { ...current, delivery: { ...current.delivery, state: "delivered" } };
|
|
1682
|
+
});
|
|
1683
|
+
foregroundDeliveries.delete(event.toolCallId);
|
|
1684
|
+
}
|
|
1685
|
+
if (event.toolName !== "workflow" || !event.isError) return;
|
|
1686
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1687
|
+
if (!diagnostic) return;
|
|
1688
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1689
|
+
return { content: [{ type: "text" as const, text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1690
|
+
});
|
|
1691
|
+
const deliverTerminal = async (store: RunStore, content: string): Promise<void> => {
|
|
1692
|
+
let claimed: boolean | undefined;
|
|
1693
|
+
await store.updateState((current) => {
|
|
1694
|
+
if (current.delivery?.state === "delivered") return current;
|
|
1695
|
+
if (!current.delivery) { claimed = true; return current; }
|
|
1696
|
+
claimed = true;
|
|
1697
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
|
|
1698
|
+
});
|
|
1699
|
+
if (claimed === true) deliver(pi, content);
|
|
1700
|
+
};
|
|
1701
|
+
const scheduleForegroundDelivery = (toolCallId: string, send: () => Promise<void>): void => {
|
|
1702
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
1703
|
+
if (!delivery || delivery.inline || typeof (pi as unknown as { sendMessage?: unknown }).sendMessage !== "function") return;
|
|
1704
|
+
//NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
|
|
1705
|
+
delivery.timer = setTimeout(() => {
|
|
1706
|
+
delete delivery.timer;
|
|
1707
|
+
void send().finally(() => foregroundDeliveries.delete(toolCallId));
|
|
1708
|
+
}, 0);
|
|
1709
|
+
};
|
|
1568
1710
|
const phaseBridge = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => {
|
|
1569
1711
|
let cursor = 0;
|
|
1570
1712
|
return async (phase: string): Promise<void> => {
|
|
@@ -1605,17 +1747,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1605
1747
|
const path = shellIdentityPath(identity);
|
|
1606
1748
|
const replayed = await store.replay(path);
|
|
1607
1749
|
if (replayed) return readShellResult(replayed.value);
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1750
|
+
const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
|
|
1751
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
|
|
1752
|
+
try {
|
|
1753
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
1754
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
1755
|
+
await store.complete(path, result as unknown as JsonValue);
|
|
1756
|
+
return result;
|
|
1757
|
+
} finally {
|
|
1758
|
+
const stopped = await persistRunState(store, metadata, (current) => {
|
|
1759
|
+
const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
|
|
1760
|
+
if (activeShells > 0) return { ...current, activeShells };
|
|
1761
|
+
const next = { ...current };
|
|
1762
|
+
delete next.activeShells;
|
|
1763
|
+
return next;
|
|
1764
|
+
});
|
|
1765
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
|
|
1766
|
+
}
|
|
1612
1767
|
} finally { await lifecycle.leave(); }
|
|
1613
1768
|
};
|
|
1614
1769
|
const lifecycleFor = (store: RunStore, state: RunState, budget: WorkflowBudgetRuntime, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
1615
1770
|
if (next !== "pausing") budget.transition(next);
|
|
1616
1771
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
1617
1772
|
const nextRun = { ...current, state: next, ...budget.snapshot() };
|
|
1618
|
-
if (next === "running" || next === "completed") delete nextRun.error;
|
|
1773
|
+
if (next === "running" || next === "completed") { delete nextRun.error; delete nextRun.failedAt; }
|
|
1619
1774
|
return nextRun;
|
|
1620
1775
|
});
|
|
1621
1776
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
@@ -1629,25 +1784,29 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1629
1784
|
const onProgress = async (progress: AgentProgress) => {
|
|
1630
1785
|
let runState: PersistedRun;
|
|
1631
1786
|
if (progress.persist) {
|
|
1632
|
-
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);
|
|
1787
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
|
|
1633
1788
|
} else {
|
|
1634
1789
|
const loaded = await run.store.load();
|
|
1635
1790
|
if (!loaded.run.agents.some((agent) => agent.id === id)) return;
|
|
1636
|
-
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) };
|
|
1791
|
+
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
|
|
1637
1792
|
}
|
|
1638
1793
|
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
1639
1794
|
setLiveActivity(runId, id, progress.activity);
|
|
1795
|
+
setLiveEventTime(runId, id, progress.lastEventAt);
|
|
1640
1796
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
1641
1797
|
};
|
|
1642
|
-
const onAttempt = async (attempt:
|
|
1798
|
+
const onAttempt = async (attempt: AgentAttempt) => {
|
|
1799
|
+
setLiveAgentSession(runId, id, attempt.liveSession);
|
|
1643
1800
|
await scheduler.flush();
|
|
1644
1801
|
scheduler.attemptStarted(id);
|
|
1802
|
+
const lastEventAt = Date.now();
|
|
1803
|
+
setLiveEventTime(runId, id, lastEventAt);
|
|
1645
1804
|
await scheduler.flush();
|
|
1646
1805
|
const before = (await run.store.load()).run;
|
|
1647
1806
|
await persistActiveAgentAttempt(run.store, id, attempt);
|
|
1648
1807
|
const active = (await run.store.load()).run;
|
|
1649
1808
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
1650
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1809
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
1651
1810
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1652
1811
|
};
|
|
1653
1812
|
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); });
|
|
@@ -1657,9 +1816,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1657
1816
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
|
|
1658
1817
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1659
1818
|
setLiveActivity(runId, id);
|
|
1819
|
+
setLiveAgentSession(runId, id);
|
|
1660
1820
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1661
1821
|
return result.value;
|
|
1662
1822
|
} catch (error) {
|
|
1823
|
+
setLiveAgentSession(runId, id);
|
|
1663
1824
|
const attempts = (error as WorkflowError & { attempts?: readonly AgentAttempt[] }).attempts;
|
|
1664
1825
|
if (attempts?.length) {
|
|
1665
1826
|
const before = (await run.store.load()).run;
|
|
@@ -1687,7 +1848,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1687
1848
|
try { effective = run.executor.resolve(requested); }
|
|
1688
1849
|
catch { 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 }; }
|
|
1689
1850
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
1690
|
-
|
|
1851
|
+
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
|
|
1852
|
+
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1691
1853
|
});
|
|
1692
1854
|
return { ...current, agents };
|
|
1693
1855
|
});
|
|
@@ -1704,6 +1866,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1704
1866
|
terminalRunStates.set(runId, run.lifecycle.state as "completed" | "failed" | "stopped");
|
|
1705
1867
|
run.checkpointResolvers.clear();
|
|
1706
1868
|
liveActivities.delete(runId);
|
|
1869
|
+
liveEventTimes.delete(runId);
|
|
1870
|
+
for (const key of liveAgentSessions.keys()) if (key.startsWith(`${runId}:`)) liveAgentSessions.delete(key);
|
|
1707
1871
|
eventPublisher.removeRun(runId);
|
|
1708
1872
|
runs.delete(runId);
|
|
1709
1873
|
};
|
|
@@ -1739,13 +1903,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1739
1903
|
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) });
|
|
1740
1904
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1741
1905
|
};
|
|
1742
|
-
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult | undefined> => {
|
|
1906
|
+
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult | undefined> => {
|
|
1743
1907
|
const run = runs.get(runId);
|
|
1744
1908
|
if (!run) return undefined;
|
|
1745
1909
|
const request = await run.store.answerWorkflowDecision(proposalId, approved);
|
|
1746
1910
|
if (!request) return undefined;
|
|
1747
1911
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1748
|
-
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
1912
|
+
const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
|
|
1749
1913
|
if (!silent) deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1750
1914
|
return result;
|
|
1751
1915
|
};
|
|
@@ -1839,7 +2003,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1839
2003
|
pi.registerTool({
|
|
1840
2004
|
name: "workflow_catalog",
|
|
1841
2005
|
label: "Workflow Catalog",
|
|
1842
|
-
description: "List reusable workflow functions, variables, and model aliases
|
|
2006
|
+
description: "List reusable workflow functions, variables, and model aliases; pass `name` to load one entry in full",
|
|
1843
2007
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
1844
2008
|
async execute(_id, params = {}) {
|
|
1845
2009
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
@@ -1856,7 +2020,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1856
2020
|
});
|
|
1857
2021
|
catalogRegistered = true;
|
|
1858
2022
|
};
|
|
1859
|
-
const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() },
|
|
2023
|
+
const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, transport);
|
|
1860
2024
|
const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
|
|
1861
2025
|
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
1862
2026
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
@@ -1896,7 +2060,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1896
2060
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1897
2061
|
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
1898
2062
|
};
|
|
1899
|
-
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string) => async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: import("./types.js").AgentIdentity) => {
|
|
2063
|
+
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: import("./types.js").AgentIdentity) => {
|
|
1900
2064
|
await lifecycle.enter();
|
|
1901
2065
|
try {
|
|
1902
2066
|
const path = agentIdentityPath(identity);
|
|
@@ -1911,6 +2075,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1911
2075
|
const thinking = parseThinking(options.thinking);
|
|
1912
2076
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1913
2077
|
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools as string[] } : {}) });
|
|
2078
|
+
if (role) await captureRole?.(role, resolved.model);
|
|
1914
2079
|
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
1915
2080
|
const tools = resolved.tools;
|
|
1916
2081
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
@@ -1935,8 +2100,22 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1935
2100
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1936
2101
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1937
2102
|
};
|
|
1938
|
-
const
|
|
2103
|
+
const recoveryUi = (context: unknown): { hasUI: boolean; ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> } } => {
|
|
2104
|
+
const host = object(context) ? context : undefined;
|
|
2105
|
+
const ui = host && object(host.ui) ? host.ui as { select?: (prompt: string, options: string[]) => Promise<string | undefined> } : {};
|
|
2106
|
+
return { hasUI: host?.hasUI === true, ui };
|
|
2107
|
+
};
|
|
2108
|
+
type ColdResumeResult = { value: JsonValue; resultPath: string };
|
|
2109
|
+
const coldResumeRun = async (run: NonNullable<ReturnType<typeof runs.get>>, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
1939
2110
|
const loaded = await run.store.load();
|
|
2111
|
+
const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
|
|
2112
|
+
if (loaded.run.activeShells !== undefined) {
|
|
2113
|
+
await persistRunState(run.store, run.metadata, (current) => {
|
|
2114
|
+
const next = { ...current };
|
|
2115
|
+
delete next.activeShells;
|
|
2116
|
+
return next;
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
1940
2119
|
await run.store.validateRetrySource();
|
|
1941
2120
|
await run.store.validateBorrowedWorktrees();
|
|
1942
2121
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
@@ -1950,7 +2129,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1950
2129
|
run.abortController = controller;
|
|
1951
2130
|
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 });
|
|
1952
2131
|
if (!script) throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
1953
|
-
|
|
2132
|
+
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
2133
|
+
await run.store.saveSnapshot(persistedSnapshot);
|
|
1954
2134
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1955
2135
|
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) });
|
|
1956
2136
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
@@ -1961,13 +2141,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1961
2141
|
try { variables = await resolveWorkflowVariables(runContext, controller, registry); }
|
|
1962
2142
|
catch (error) {
|
|
1963
2143
|
const typed = asWorkflowError(error);
|
|
1964
|
-
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) { await run.lifecycle.terminal("failed", typed.code).catch(() => undefined); const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current
|
|
2144
|
+
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) { await run.lifecycle.terminal("failed", typed.code).catch(() => undefined); const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed)); await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed"); run.update?.(workflowToolUpdate(persisted)); if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted).then((diagnostic) => { deliverFailure(pi, diagnostic); }).catch(() => undefined); }
|
|
1965
2145
|
await cleanupTerminalRun(run.store.runId);
|
|
1966
2146
|
throw typed;
|
|
1967
2147
|
}
|
|
1968
2148
|
await scheduler.cancelRun(run.store.runId);
|
|
1969
2149
|
await run.lifecycle.resume();
|
|
1970
|
-
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,
|
|
2150
|
+
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);
|
|
1971
2151
|
run.execution = execution;
|
|
1972
2152
|
const completion = execution.result.then(async (value) => {
|
|
1973
2153
|
await scheduler.flush();
|
|
@@ -1980,17 +2160,28 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1980
2160
|
await scheduler.flush();
|
|
1981
2161
|
const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
|
|
1982
2162
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
1983
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot()
|
|
2163
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
|
|
1984
2164
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
1985
2165
|
if (state === "failed") retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
|
|
1986
2166
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
1987
2167
|
run.update?.(workflowToolUpdate(persisted));
|
|
1988
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted).
|
|
2168
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) { const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted); Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic }); }
|
|
2169
|
+
throw typed;
|
|
1989
2170
|
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
1990
2171
|
run.completion = completion;
|
|
1991
|
-
|
|
2172
|
+
if (!foreground || !waitForCompletion) {
|
|
2173
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2174
|
+
deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
|
|
2175
|
+
}, (error: unknown) => {
|
|
2176
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2177
|
+
if (diagnostic) deliverFailure(pi, diagnostic);
|
|
2178
|
+
else deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
|
|
2179
|
+
});
|
|
2180
|
+
return undefined;
|
|
2181
|
+
}
|
|
2182
|
+
return completion;
|
|
1992
2183
|
};
|
|
1993
|
-
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult> => {
|
|
2184
|
+
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult> => {
|
|
1994
2185
|
const run = runs.get(request.runId);
|
|
1995
2186
|
if (!run) throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
1996
2187
|
if (!approved) return { state: "budget_exhausted", approved: false };
|
|
@@ -1999,10 +2190,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1999
2190
|
const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
|
|
2000
2191
|
run.budget = runtime;
|
|
2001
2192
|
await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
|
|
2002
|
-
|
|
2193
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2194
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
|
|
2195
|
+
if (completed) return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
|
|
2003
2196
|
return { state: "running", approved: true };
|
|
2004
2197
|
};
|
|
2005
|
-
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal): Promise<Record<string, JsonValue>> => {
|
|
2198
|
+
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal, modeOverride?: boolean, waitForCompletion = true): Promise<Record<string, JsonValue>> => {
|
|
2006
2199
|
const run = runs.get(runId);
|
|
2007
2200
|
if (!run) {
|
|
2008
2201
|
const host = object(context) ? context : {};
|
|
@@ -2041,11 +2234,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2041
2234
|
run.budget = runtime;
|
|
2042
2235
|
await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
|
|
2043
2236
|
}
|
|
2044
|
-
|
|
2237
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2238
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
|
|
2239
|
+
if (completed) return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run as unknown as JsonValue };
|
|
2045
2240
|
return { state: "running" };
|
|
2046
2241
|
};
|
|
2047
2242
|
const retryReservations = new Set<string>();
|
|
2048
|
-
const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal): Promise<{ runId: string; parentRunId: string; state: "running" }> => {
|
|
2243
|
+
const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal, modeOverride?: boolean): Promise<{ runId: string; parentRunId: string; state: "running" | "completed"; value?: JsonValue; run?: PersistedRun }> => {
|
|
2049
2244
|
if (typeof runId !== "string" || !runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
2050
2245
|
const host = object(context) ? context : {};
|
|
2051
2246
|
const cwd = typeof host.cwd === "string" ? host.cwd : undefined;
|
|
@@ -2098,7 +2293,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2098
2293
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
2099
2294
|
const childInitialBudget = childBudget.snapshot();
|
|
2100
2295
|
const retry: WorkflowRetryProvenance = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
2101
|
-
await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [],
|
|
2296
|
+
await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
|
|
2102
2297
|
const fallbackModel: ModelSpec = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
2103
2298
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
|
|
2104
2299
|
const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
|
|
@@ -2109,12 +2304,17 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2109
2304
|
runs.set(childRunId, childRun);
|
|
2110
2305
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2111
2306
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
2112
|
-
|
|
2307
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2308
|
+
const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
2113
2309
|
const completion = runs.get(childRunId)?.completion;
|
|
2114
2310
|
if (completion) {
|
|
2115
2311
|
childStarted = true;
|
|
2116
2312
|
void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
|
|
2313
|
+
} else if (completed) {
|
|
2314
|
+
childStarted = true;
|
|
2315
|
+
retryReservations.delete(lineageRootRunId);
|
|
2117
2316
|
}
|
|
2317
|
+
if (completed) return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
|
|
2118
2318
|
return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
|
|
2119
2319
|
} finally {
|
|
2120
2320
|
if (!childStarted) retryReservations.delete(lineageRootRunId);
|
|
@@ -2126,7 +2326,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2126
2326
|
description: "Retry a failed workflow run by replaying its completed structural operations",
|
|
2127
2327
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
2128
2328
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2129
|
-
try { const result = await retryWorkflowRun(params.runId, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2329
|
+
try { const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2130
2330
|
catch (error) { throw mainAgentError(error); }
|
|
2131
2331
|
},
|
|
2132
2332
|
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_retry", args, theme)); },
|
|
@@ -2136,9 +2336,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2136
2336
|
name: "workflow_resume",
|
|
2137
2337
|
label: "Workflow Resume",
|
|
2138
2338
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
2139
|
-
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
2339
|
+
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 }),
|
|
2140
2340
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2141
|
-
try { const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2341
|
+
try { const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2142
2342
|
catch (error) { throw mainAgentError(error); }
|
|
2143
2343
|
},
|
|
2144
2344
|
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_resume", args, theme)); },
|
|
@@ -2159,10 +2359,23 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2159
2359
|
if (loaded.run.state === "completed" || loaded.run.state === "failed" || loaded.run.state === "stopped") { terminalRunStates.set(runId, loaded.run.state); continue; }
|
|
2160
2360
|
if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
|
|
2161
2361
|
const previousState = loaded.run.state;
|
|
2162
|
-
await store.updateState((current) =>
|
|
2362
|
+
await store.updateState((current) => {
|
|
2363
|
+
if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state)) return current;
|
|
2364
|
+
const next = { ...current, state: "interrupted" as const };
|
|
2365
|
+
delete next.activeShells;
|
|
2366
|
+
return next;
|
|
2367
|
+
});
|
|
2163
2368
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2164
2369
|
await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
|
|
2165
2370
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2371
|
+
} else if (loaded.run.activeShells !== undefined) {
|
|
2372
|
+
await store.updateState((current) => {
|
|
2373
|
+
if (["completed", "failed", "stopped"].includes(current.state)) return current;
|
|
2374
|
+
const next = { ...current };
|
|
2375
|
+
delete next.activeShells;
|
|
2376
|
+
return next;
|
|
2377
|
+
});
|
|
2378
|
+
loaded = { ...loaded, run: (await store.load()).run };
|
|
2166
2379
|
}
|
|
2167
2380
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
|
|
2168
2381
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
@@ -2188,7 +2401,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2188
2401
|
if (choice && choice !== "Skip") {
|
|
2189
2402
|
const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
|
|
2190
2403
|
for (const run of toResume) {
|
|
2191
|
-
try { await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx); ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info"); }
|
|
2404
|
+
try { await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false); ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info"); }
|
|
2192
2405
|
catch (err) { ctx.ui.notify(`Cannot resume ${run.metadata.name}: ${err instanceof Error ? err.message : String(err)}`, "warning"); }
|
|
2193
2406
|
}
|
|
2194
2407
|
}
|
|
@@ -2245,10 +2458,24 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2245
2458
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2246
2459
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
2247
2460
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
2248
|
-
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" as const, 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 });
|
|
2461
|
+
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" as const, 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 });
|
|
2462
|
+
let persistedSnapshot = snapshot;
|
|
2463
|
+
const captureFunctionRole = functionName ? async (role: string, model: ModelSpec): Promise<void> => {
|
|
2464
|
+
const definition = agentDefinitions[role];
|
|
2465
|
+
if (!definition) return;
|
|
2466
|
+
const modelName = `${model.provider}/${model.model}`;
|
|
2467
|
+
const hasProjectRole = projectAgentDefinitions[role] !== undefined;
|
|
2468
|
+
if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName)) return;
|
|
2469
|
+
const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
|
|
2470
|
+
const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
|
|
2471
|
+
const models = [...new Set([...persistedSnapshot.models, modelName])];
|
|
2472
|
+
persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
|
|
2473
|
+
await store.saveSnapshot(persistedSnapshot);
|
|
2474
|
+
} : undefined;
|
|
2249
2475
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2250
2476
|
const initialBudget = budgetRuntime.snapshot();
|
|
2251
|
-
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [],
|
|
2477
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2478
|
+
if (params.foreground) foregroundDeliveries.set(toolCallId, { store, inline: false });
|
|
2252
2479
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2253
2480
|
const background = !params.foreground;
|
|
2254
2481
|
const providerPause = async () => { if (background) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
@@ -2257,7 +2484,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2257
2484
|
runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
|
|
2258
2485
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2259
2486
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2260
|
-
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);
|
|
2487
|
+
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);
|
|
2261
2488
|
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).execution = execution;
|
|
2262
2489
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2263
2490
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2271,7 +2498,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2271
2498
|
await scheduler.flush();
|
|
2272
2499
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2273
2500
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state)) await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2274
|
-
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot()
|
|
2501
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
|
|
2275
2502
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2276
2503
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
2277
2504
|
const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
@@ -2281,16 +2508,37 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2281
2508
|
});
|
|
2282
2509
|
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
2283
2510
|
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).completion = completion;
|
|
2511
|
+
const deliverFailureContent = (error: unknown): string => {
|
|
2512
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2513
|
+
return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
|
|
2514
|
+
};
|
|
2515
|
+
const queueForegroundDelivery = async (content: string): Promise<void> => {
|
|
2516
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
2517
|
+
if (!delivery) return;
|
|
2518
|
+
await store.updateState((current) => {
|
|
2519
|
+
if (!current.delivery || current.delivery.state === "delivered") return current;
|
|
2520
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
|
|
2521
|
+
});
|
|
2522
|
+
if (delivery.inline) return;
|
|
2523
|
+
scheduleForegroundDelivery(toolCallId, async () => {
|
|
2524
|
+
if (delivery.inline) return;
|
|
2525
|
+
pendingFailureDiagnostics.delete(toolCallId);
|
|
2526
|
+
await deliverTerminal(store, content);
|
|
2527
|
+
});
|
|
2528
|
+
};
|
|
2284
2529
|
if (background) {
|
|
2285
2530
|
void completion.then(async ({ value, resultPath }) => {
|
|
2286
|
-
|
|
2287
|
-
}, (error: unknown) => {
|
|
2288
|
-
|
|
2289
|
-
if (diagnostic) deliverFailure(pi, diagnostic);
|
|
2290
|
-
else deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
2531
|
+
await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2532
|
+
}, async (error: unknown) => {
|
|
2533
|
+
await deliverTerminal(store, deliverFailureContent(error));
|
|
2291
2534
|
});
|
|
2292
2535
|
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2293
2536
|
}
|
|
2537
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2538
|
+
await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2539
|
+
}, async (error: unknown) => {
|
|
2540
|
+
await queueForegroundDelivery(deliverFailureContent(error));
|
|
2541
|
+
});
|
|
2294
2542
|
const { value } = await completion;
|
|
2295
2543
|
const run = (await store.load()).run;
|
|
2296
2544
|
return { content: [{ type: "text" as const, text: JSON.stringify(value) }, { type: "text" as const, text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
@@ -2305,7 +2553,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2305
2553
|
const details = result.details;
|
|
2306
2554
|
if (isWorkflowFailureDiagnostics(details)) return textBlock(formatWorkflowFailureDiagnostics(details));
|
|
2307
2555
|
const runDetails = details as { run?: PersistedRun; value?: JsonValue; preview?: string } | undefined;
|
|
2308
|
-
const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval
|
|
2556
|
+
const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval>; workflowProgress?: WorkflowProgressRefreshState };
|
|
2309
2557
|
if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
|
|
2310
2558
|
state.workflowSpinner = setInterval(context.invalidate, 80);
|
|
2311
2559
|
state.workflowSpinner.unref();
|
|
@@ -2313,7 +2561,31 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2313
2561
|
clearInterval(state.workflowSpinner);
|
|
2314
2562
|
delete state.workflowSpinner;
|
|
2315
2563
|
}
|
|
2316
|
-
if (runDetails?.run)
|
|
2564
|
+
if (runDetails?.run) {
|
|
2565
|
+
const incoming = runDetails.run;
|
|
2566
|
+
let progress = state.workflowProgress;
|
|
2567
|
+
if (!isPartial || !progress || progress.runId !== incoming.id) {
|
|
2568
|
+
progress = undefined;
|
|
2569
|
+
delete state.workflowProgress;
|
|
2570
|
+
if (isPartial) {
|
|
2571
|
+
progress = { runId: incoming.id, inputRun: incoming, run: incoming, lastRefreshAt: 0, runtimeStartedAt: Date.now(), runtimeBaseMs: incoming.usage?.durationMs ?? 0 };
|
|
2572
|
+
state.workflowProgress = progress;
|
|
2573
|
+
}
|
|
2574
|
+
} else if (progress.inputRun !== incoming) {
|
|
2575
|
+
if (progress.run.state !== "running" && incoming.state === "running") {
|
|
2576
|
+
progress.runtimeBaseMs = incoming.usage?.durationMs ?? 0;
|
|
2577
|
+
progress.runtimeStartedAt = Date.now();
|
|
2578
|
+
}
|
|
2579
|
+
progress.inputRun = incoming;
|
|
2580
|
+
progress.run = incoming;
|
|
2581
|
+
}
|
|
2582
|
+
return workflowProgressBlock(progress?.run ?? incoming, theme, progress, async () => {
|
|
2583
|
+
const active = runs.get(incoming.id);
|
|
2584
|
+
const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
|
|
2585
|
+
const loaded = await store.load();
|
|
2586
|
+
return withLiveActivities(loaded.run);
|
|
2587
|
+
}, () => { if (state.workflowProgress === progress) context.invalidate(); });
|
|
2588
|
+
}
|
|
2317
2589
|
const content = result.content[0];
|
|
2318
2590
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
2319
2591
|
},
|
|
@@ -2332,7 +2604,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2332
2604
|
const loadStores = async () => {
|
|
2333
2605
|
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
2334
2606
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
2335
|
-
try { return { store, loaded:
|
|
2607
|
+
try { const loaded = await store.load(); return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } }; }
|
|
2336
2608
|
catch { if (!await store.isComplete()) await store.delete(true).catch(() => undefined); return undefined; }
|
|
2337
2609
|
}));
|
|
2338
2610
|
return entries.filter((entry): entry is { store: RunStore; loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> } } => entry !== undefined);
|
|
@@ -2355,7 +2627,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2355
2627
|
return keepContext ? "dashboard" : "done";
|
|
2356
2628
|
}
|
|
2357
2629
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2358
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2630
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
2359
2631
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2360
2632
|
return keepContext ? "dashboard" : "done";
|
|
2361
2633
|
}
|
|
@@ -2368,10 +2640,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2368
2640
|
if (action === "resume" && run) {
|
|
2369
2641
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2370
2642
|
const patch: unknown = rest.length ? JSON.parse(rest.join(" ")) as unknown : undefined;
|
|
2371
|
-
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2372
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
2643
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
2644
|
+
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");
|
|
2373
2645
|
} else {
|
|
2374
|
-
if (run.lifecycle.state === "interrupted") await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2646
|
+
if (run.lifecycle.state === "interrupted") await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2375
2647
|
else {
|
|
2376
2648
|
if (run.lifecycle.state === "paused") await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
2377
2649
|
await run.lifecycle.resume();
|
|
@@ -2383,8 +2655,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2383
2655
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
2384
2656
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}" );
|
|
2385
2657
|
if (input === undefined) return keepContext ? "dashboard" : "done";
|
|
2386
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2387
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
2658
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
2659
|
+
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");
|
|
2388
2660
|
return keepContext ? "dashboard" : "done";
|
|
2389
2661
|
}
|
|
2390
2662
|
if (action === "stop" && run) {
|
|
@@ -2528,11 +2800,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2528
2800
|
};
|
|
2529
2801
|
const loadDashboard = async () => {
|
|
2530
2802
|
const loaded = await store.load();
|
|
2803
|
+
const liveRun = withLiveActivities(loaded.run);
|
|
2531
2804
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2532
2805
|
const worktrees = await store.worktrees();
|
|
2533
2806
|
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
2534
2807
|
const agentResults = new Map<string, JsonValue>();
|
|
2535
|
-
for (const agent of
|
|
2808
|
+
for (const agent of liveRun.agents) {
|
|
2536
2809
|
if (agent.state !== "completed" || agent.parentId || !agent.resultPath) continue;
|
|
2537
2810
|
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
2538
2811
|
if (operation) agentResults.set(agent.id, operation.value);
|
|
@@ -2542,15 +2815,15 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2542
2815
|
const reviews = new Map<string, AwaitingCheckpoint>();
|
|
2543
2816
|
const add = (label: string, value: string) => { actions.set(label, `${value} ${store.runId}`); };
|
|
2544
2817
|
const addCopy = (label: string, value: string, artifact: string) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
2545
|
-
if (
|
|
2546
|
-
if (["paused", "interrupted"].includes(
|
|
2547
|
-
if (
|
|
2818
|
+
if (liveRun.state === "running") add("Pause", "pause");
|
|
2819
|
+
if (["paused", "interrupted"].includes(liveRun.state)) add("Resume", "resume");
|
|
2820
|
+
if (liveRun.state === "budget_exhausted") { actions.set("Resume unchanged", `resume ${store.runId}`); actions.set("Adjust budget", `adjust ${store.runId}`); }
|
|
2548
2821
|
for (const decision of await store.pendingWorkflowDecisions()) {
|
|
2549
2822
|
const id = decision.proposalId.slice(0, 8);
|
|
2550
2823
|
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
2551
2824
|
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
2552
2825
|
}
|
|
2553
|
-
if (!terminalStates.has(
|
|
2826
|
+
if (!terminalStates.has(liveRun.state)) add("Stop", "stop");
|
|
2554
2827
|
for (const cp of checkpoints) {
|
|
2555
2828
|
if (ctx.mode === "tui") {
|
|
2556
2829
|
const label = `Review ${cp.name}`;
|
|
@@ -2563,42 +2836,41 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2563
2836
|
}
|
|
2564
2837
|
if (ctx.mode !== "tui") actions.set("Refresh", "refresh");
|
|
2565
2838
|
else actions.set("Open script in editor", "open-script");
|
|
2566
|
-
if (ctx.mode !== "tui" &&
|
|
2567
|
-
if (terminalStates.has(
|
|
2839
|
+
if (ctx.mode !== "tui" && liveRun.agents.length) actions.set("Agents...", "agents");
|
|
2840
|
+
if (terminalStates.has(liveRun.state)) add("Delete", "delete");
|
|
2568
2841
|
if (ctx.mode === "tui") {
|
|
2569
2842
|
addCopy("Copy run path", store.directory, "run path");
|
|
2570
2843
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2571
2844
|
}
|
|
2572
|
-
return { dashboard: formatWorkflowPhaseDashboard(
|
|
2845
|
+
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 };
|
|
2573
2846
|
};
|
|
2574
2847
|
const agentWorktreeFor = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): WorktreeReference | undefined => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
2848
|
+
const agentAttemptActionContext = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): AgentAttemptActionContext | undefined => {
|
|
2849
|
+
const attempt = (agent.attemptDetails ?? []).reduce<AgentAttemptSummary | undefined>((latest, candidate) => !latest || candidate.attempt > latest.attempt ? candidate : latest, undefined);
|
|
2850
|
+
if (!attempt) return undefined;
|
|
2851
|
+
const liveCandidate = liveAgentSessions.get(`${dashboard.run.id}:${agent.id}`);
|
|
2852
|
+
const live = liveCandidate && attempt.session && liveCandidate.reference.transport === attempt.session.transport && liveCandidate.reference.sessionId === attempt.session.sessionId ? liveCandidate : undefined;
|
|
2853
|
+
const run = runs.get(dashboard.run.id);
|
|
2854
|
+
const ui = { notify: (message: string, level: "info" | "warning" | "error" = "info") => { ctx.ui.notify(message, level); }, confirm: (title: string, message: string) => ctx.ui.confirm(title, message), select: (title: string, options: readonly string[]) => { return ctx.ui.select(title, [...options]); }, input: (title: string, placeholder?: string) => ctx.ui.input(title, placeholder) };
|
|
2855
|
+
const attemptSnapshot = deepFreeze(structuredClone(attempt));
|
|
2856
|
+
return { run: deepFreeze(structuredClone(dashboard.run)), agent: deepFreeze(structuredClone(agent)), attempt: attemptSnapshot, ...(attemptSnapshot.session ? { session: attemptSnapshot.session } : {}), ...(live ? { liveSession: live } : {}), signal: run?.abortController.signal ?? new AbortController().signal, ui: Object.freeze(ui) };
|
|
2857
|
+
};
|
|
2858
|
+
const visibleAgentAttemptActions = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): readonly [string, import("./types.js").AgentAttemptAction][] => {
|
|
2859
|
+
const context = agentAttemptActionContext(dashboard, agent);
|
|
2860
|
+
if (!context) return [];
|
|
2861
|
+
return Object.entries(registry.agentAttemptActions()).filter(([, action]) => { try { return action.visible(context); } catch { return false; } });
|
|
2862
|
+
};
|
|
2575
2863
|
const agentActionLabels = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): string[] => {
|
|
2576
2864
|
const worktree = agentWorktreeFor(dashboard, agent);
|
|
2577
2865
|
return [
|
|
2578
|
-
...(agent.
|
|
2866
|
+
...visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
|
|
2579
2867
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
2868
|
+
...(ctx.mode === "tui" && agent.prompt !== undefined ? ["Open prompt in editor"] : []),
|
|
2580
2869
|
...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
|
|
2581
2870
|
"Copy agent ID",
|
|
2582
2871
|
"Back",
|
|
2583
2872
|
];
|
|
2584
2873
|
};
|
|
2585
|
-
const forkAgentSession = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): Promise<void> => {
|
|
2586
|
-
const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
2587
|
-
const worktree = agentWorktreeFor(dashboard, agent);
|
|
2588
|
-
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
2589
|
-
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
2590
|
-
const index = choice ? choices.indexOf(choice) : -1;
|
|
2591
|
-
const attempt = index >= 0 ? attempts[index] : undefined;
|
|
2592
|
-
if (!attempt) return;
|
|
2593
|
-
const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
2594
|
-
if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?")) return;
|
|
2595
|
-
try {
|
|
2596
|
-
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
2597
|
-
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
2598
|
-
} catch (error) {
|
|
2599
|
-
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
2600
|
-
}
|
|
2601
|
-
};
|
|
2602
2874
|
const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, requestedAgentId?: string): Promise<void> => {
|
|
2603
2875
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
2604
2876
|
const title = (agent: AgentRecord): string => agentBreadcrumb(agent, byId, true);
|
|
@@ -2616,10 +2888,15 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2616
2888
|
for (;;) {
|
|
2617
2889
|
const action = await ctx.ui.select(title(selected), actions);
|
|
2618
2890
|
if (!action || action === "Back") return;
|
|
2891
|
+
const extensionAction = visibleAgentAttemptActions(dashboard, selected).find(([, candidate]) => candidate.label === action);
|
|
2892
|
+
if (extensionAction) {
|
|
2893
|
+
const context = agentAttemptActionContext(dashboard, selected);
|
|
2894
|
+
if (context) { try { await extensionAction[1].run(context); } catch (error) { ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error"); } }
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2619
2897
|
if (action === "Copy agent ID") { await copyArtifact(selected.id, "agent ID"); continue; }
|
|
2620
2898
|
if (action === "Copy branch" && worktree) { await copyArtifact(worktree.branch, "branch"); continue; }
|
|
2621
2899
|
if (action === "Copy worktree path" && worktree) { await copyArtifact(worktree.path, "worktree path"); continue; }
|
|
2622
|
-
if (action === "Fork as Pi session in pane") await forkAgentSession(dashboard, selected);
|
|
2623
2900
|
}
|
|
2624
2901
|
};
|
|
2625
2902
|
for (;;) {
|
|
@@ -2641,12 +2918,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2641
2918
|
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
2642
2919
|
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
2643
2920
|
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
2644
|
-
const terminalRows = () => Math.max(1, tuiRows(tui) -
|
|
2921
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
2645
2922
|
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
2646
|
-
const keyLabel = (binding: string, fallback: string) =>
|
|
2647
|
-
const keys = keybindingKeys(keybindings, binding);
|
|
2648
|
-
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
2649
|
-
};
|
|
2923
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
2650
2924
|
const selectedAgentRecord = (): AgentRecord | undefined => {
|
|
2651
2925
|
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
2652
2926
|
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
@@ -2671,7 +2945,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2671
2945
|
ctx.ui.notify(`Cannot open ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
2672
2946
|
} finally {
|
|
2673
2947
|
editorRunning = false;
|
|
2674
|
-
tui.requestRender(true);
|
|
2675
2948
|
}
|
|
2676
2949
|
};
|
|
2677
2950
|
const updateDashboard = async () => {
|
|
@@ -2718,7 +2991,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2718
2991
|
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
2719
2992
|
}, 1000);
|
|
2720
2993
|
timer.unref();
|
|
2721
|
-
return
|
|
2994
|
+
return {
|
|
2722
2995
|
render(width: number) {
|
|
2723
2996
|
renderedWidth = width;
|
|
2724
2997
|
const narrow = width < 80;
|
|
@@ -2749,7 +3022,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2749
3022
|
selectionNeedsScroll = false;
|
|
2750
3023
|
}
|
|
2751
3024
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
2752
|
-
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" : "
|
|
3025
|
+
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
2753
3026
|
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
2754
3027
|
},
|
|
2755
3028
|
invalidate() {},
|
|
@@ -2759,25 +3032,35 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2759
3032
|
if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
|
|
2760
3033
|
if (actionMode) {
|
|
2761
3034
|
const options = actionOptions();
|
|
2762
|
-
if (keybindings
|
|
2763
|
-
if (keybindings
|
|
2764
|
-
else if (keybindings
|
|
2765
|
-
else if (keybindings
|
|
2766
|
-
else if (keybindings
|
|
2767
|
-
else if (keybindings
|
|
3035
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) { actionMode = false; dashboardOffset = 0; tui.requestRender(); return; }
|
|
3036
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
3037
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) actionIndex = (actionIndex + 1) % options.length;
|
|
3038
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3039
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3040
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2768
3041
|
const action = options[actionIndex];
|
|
2769
3042
|
const agent = selectedAgentRecord();
|
|
2770
3043
|
if (!action || action === "Back") { actionMode = false; dashboardOffset = 0; }
|
|
2771
3044
|
else if (agent) {
|
|
2772
3045
|
const worktree = agentWorktreeFor(view, agent);
|
|
2773
|
-
if (action === "Open
|
|
3046
|
+
if (action === "Open prompt in editor") {
|
|
3047
|
+
if (agent.prompt !== undefined) void openArtifact(Promise.resolve(workflowPromptArtifact(agent.prompt)), "agent prompt");
|
|
3048
|
+
}
|
|
3049
|
+
else if (action === "Open result in editor") {
|
|
2774
3050
|
const result = view.agentResults.get(agent.id);
|
|
2775
3051
|
if (result !== undefined) void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
|
|
2776
3052
|
}
|
|
2777
3053
|
else if (action === "Copy agent ID") void copyArtifact(agent.id, "agent ID");
|
|
2778
3054
|
else if (action === "Copy branch" && worktree) void copyArtifact(worktree.branch, "branch");
|
|
2779
3055
|
else if (action === "Copy worktree path" && worktree) void copyArtifact(worktree.path, "worktree path");
|
|
2780
|
-
else
|
|
3056
|
+
else {
|
|
3057
|
+
const extensionAction = visibleAgentAttemptActions(view, agent).find(([, candidate]) => candidate.label === action);
|
|
3058
|
+
const actionContext = extensionAction ? agentAttemptActionContext(view, agent) : undefined;
|
|
3059
|
+
if (extensionAction && actionContext) {
|
|
3060
|
+
actionMode = false;
|
|
3061
|
+
void Promise.resolve(extensionAction[1].run(actionContext)).catch((error: unknown) => { ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error"); }).finally(() => { void updateDashboard(); });
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
2781
3064
|
}
|
|
2782
3065
|
else if (action === "Open script in editor") void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
|
|
2783
3066
|
else if (action === "Stop") requestStop();
|
|
@@ -2787,30 +3070,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2787
3070
|
return;
|
|
2788
3071
|
}
|
|
2789
3072
|
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
2790
|
-
if (keybindings
|
|
2791
|
-
if (narrow && detailsMode) { detailsMode = false; selectionNeedsScroll = true; } else done(
|
|
3073
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
3074
|
+
if (narrow && detailsMode) { detailsMode = false; selectionNeedsScroll = true; } else done("Back");
|
|
2792
3075
|
} else if (narrow && detailsMode) {
|
|
2793
|
-
if (keybindings
|
|
2794
|
-
else if (keybindings
|
|
2795
|
-
else if (keybindings
|
|
3076
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3077
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3078
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2796
3079
|
if (current?.kind === "agent" && current.agentId) { actionMode = true; actionIndex = 0; }
|
|
2797
3080
|
else if (current?.children.length) { if (expandedNodeIds.has(current.id)) expandedNodeIds.delete(current.id); else expandedNodeIds.add(current.id); }
|
|
2798
3081
|
}
|
|
2799
|
-
} else if (keybindings
|
|
3082
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
2800
3083
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
2801
3084
|
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
2802
|
-
} else if (keybindings
|
|
3085
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
2803
3086
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
2804
3087
|
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
2805
|
-
} else if (keybindings
|
|
3088
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
2806
3089
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
2807
3090
|
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
2808
|
-
} else if (keybindings
|
|
3091
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
2809
3092
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
2810
3093
|
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
2811
|
-
} else if (keybindings
|
|
2812
|
-
else if (keybindings
|
|
2813
|
-
else if (keybindings
|
|
3094
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3095
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3096
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2814
3097
|
if (narrow) detailsMode = true;
|
|
2815
3098
|
else if (current?.kind === "agent" && current.agentId) { actionMode = true; actionIndex = 0; }
|
|
2816
3099
|
else if (current?.children.length) { if (expandedNodeIds.has(current.id)) expandedNodeIds.delete(current.id); else expandedNodeIds.add(current.id); }
|
|
@@ -2818,18 +3101,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2818
3101
|
tui.requestRender();
|
|
2819
3102
|
},
|
|
2820
3103
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "
|
|
2824
|
-
if (!actionChoice || actionChoice === "
|
|
3104
|
+
};
|
|
3105
|
+
})
|
|
3106
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
3107
|
+
if (!actionChoice || actionChoice === "Back") { stores = await loadStores(); break; }
|
|
2825
3108
|
if (actionChoice === "Agents...") { await selectAgent(view); continue; }
|
|
2826
3109
|
if (actionChoice.startsWith("__workflow_agent__:")) { await selectAgent(view, actionChoice.slice("__workflow_agent__:".length)); continue; }
|
|
2827
|
-
if (actionChoice.startsWith("__workflow_fork__:")) {
|
|
2828
|
-
const agentId = actionChoice.slice("__workflow_fork__:".length);
|
|
2829
|
-
const agent = view.agents.find((candidate) => candidate.id === agentId);
|
|
2830
|
-
if (agent) await forkAgentSession(view, agent);
|
|
2831
|
-
continue;
|
|
2832
|
-
}
|
|
2833
3110
|
if (actionChoice === "Refresh") continue;
|
|
2834
3111
|
const copy = view.copies.get(actionChoice);
|
|
2835
3112
|
if (copy) { await copyArtifact(copy.value, copy.artifact); continue; }
|
|
@@ -2861,7 +3138,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2861
3138
|
const currentLayout = layout();
|
|
2862
3139
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
2863
3140
|
offset = Math.min(offset, maxOffset);
|
|
2864
|
-
const
|
|
3141
|
+
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3142
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3143
|
+
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] ?? "";
|
|
2865
3144
|
const controls = currentLayout.compactControls
|
|
2866
3145
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
2867
3146
|
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
@@ -2875,12 +3154,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2875
3154
|
},
|
|
2876
3155
|
invalidate() {},
|
|
2877
3156
|
handleInput(data: string) {
|
|
2878
|
-
if (keybindings
|
|
2879
|
-
else if (keybindings
|
|
2880
|
-
else if (keybindings
|
|
2881
|
-
else if (keybindings
|
|
2882
|
-
else if (keybindings
|
|
2883
|
-
else if (keybindings
|
|
3157
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
3158
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) selectedIndex = (selectedIndex + 1) % options.length;
|
|
3159
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) move(-layout().contentViewport);
|
|
3160
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) move(layout().contentViewport);
|
|
3161
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex] as "Approve" | "Reject");
|
|
3162
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) done(undefined);
|
|
2884
3163
|
tui.requestRender();
|
|
2885
3164
|
},
|
|
2886
3165
|
}, theme);
|