pi-extensible-workflows 3.3.0 → 3.4.1

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/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 { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery, type SessionFactory } from "./agent-execution.js";
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_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 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"]);
@@ -165,13 +165,13 @@ export class RunLifecycle {
165
165
  export function formatWorkflowPreview(args: { script?: unknown; workflow?: unknown; name?: unknown; description?: unknown }): string {
166
166
  const explicitName = typeof args.name === "string" && args.name.trim() ? args.name.trim() : undefined;
167
167
  const registeredName = typeof args.workflow === "string" && args.workflow.trim() ? args.workflow.trim() : undefined;
168
- const name = registeredName ?? explicitName ?? "workflow";
169
- if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ? "\nRegistered function" : ""}`;
168
+ const name = explicitName ?? registeredName ?? "workflow";
169
+ if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
170
170
  return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
171
171
  }
172
172
  export const WORKFLOW_TOOL_LABEL = "Workflow";
173
- export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline 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 launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes."
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."
175
175
  function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState): string {
176
176
  if (action === "resume") {
177
177
  if (state === "failed") return "Failed workflow runs must use workflow_retry({ runId })";
@@ -187,10 +187,11 @@ function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState):
187
187
  return `Only failed workflow runs can be retried; source is ${state}`;
188
188
  }
189
189
  export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
190
- name: Type.Optional(Type.String({ description: "Required non-empty name for the default inline workflow path; invalid for registered function launches" })),
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" })),
191
191
  description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
192
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(...)" })),
193
- workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name" })),
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" })),
194
195
  args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
195
196
  foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
196
197
  concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
@@ -484,12 +485,24 @@ function styleForState(map: Record<string, ProgressStyleKey>, state: string, sty
484
485
  function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
485
486
  function workflowIconStyle(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
486
487
  function phaseStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PHASE_STATE_STYLE, state, styles); }
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
+ }
487
498
  export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string {
488
499
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
489
500
  const workflowIcon = runStateGlyph(run.state, spinner);
490
501
  const iconStyle = workflowIconStyle(run.state, styles);
491
502
  const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
492
- const lines = [`${iconStyle(workflowIcon)} ${header}`];
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}`];
493
506
  const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
494
507
  lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
495
508
  const activeShells = run.activeShells ?? 0;
@@ -522,6 +535,7 @@ function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
522
535
  }
523
536
 
524
537
  const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
538
+ const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
525
539
 
526
540
  function textBlock(text: string) {
527
541
  return {
@@ -738,14 +752,38 @@ function themeWorkflowProgressStyles(theme: Theme): WorkflowProgressStyles {
738
752
  bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
739
753
  };
740
754
  }
741
- function workflowProgressBlock(run: PersistedRun, theme: Theme) {
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) {
742
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
+ };
743
764
  return {
744
765
  render(width: number) {
745
766
  const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
746
- return truncateWorkflowProgress(formatWorkflowProgress(run, frame, styles), width);
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
+ });
747
786
  },
748
- invalidate() {},
749
787
  };
750
788
  }
751
789
  export function formatBudgetStatus(run: Pick<PersistedRun, "budget" | "budgetVersion" | "usage" | "budgetEvents">): string[] {
@@ -918,7 +956,7 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
918
956
  if (!checkpoints.length) lines.push(" (none)");
919
957
  for (const checkpoint of checkpoints) lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
920
958
  lines.push(`Worktrees: ${String(worktrees.length)}`);
921
- lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
959
+ lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
922
960
  return lines.join("\n");
923
961
  }
924
962
  export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string[] {
@@ -970,7 +1008,7 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
970
1008
  const agent = node.agent;
971
1009
  if (!agent) return [styles.muted("Agent details are unavailable")];
972
1010
  const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
973
- const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
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")])];
974
1012
  const error = agent.attemptDetails?.at(-1)?.error;
975
1013
  if (error) result.push(styles.error(`Error: ${error.code}: ${error.message}`));
976
1014
  if (agent.activity) result.push(`Activity: ${agent.activity.text}`);
@@ -1052,8 +1090,8 @@ function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): W
1052
1090
  ...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
1053
1091
  structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
1054
1092
  attempt: value.failedAgent.attempt,
1055
- ...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
1056
- ...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
1093
+ ...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
1094
+ ...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
1057
1095
  } } : {}),
1058
1096
  completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
1059
1097
  id: utf8Prefix(agent.id, 128),
@@ -1079,8 +1117,6 @@ function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): W
1079
1117
  bounded = { ...bounded, retry };
1080
1118
  continue;
1081
1119
  }
1082
- if (bounded.failedAgent?.sessionFile) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionFile; bounded = { ...bounded, failedAgent }; continue; }
1083
- if (bounded.failedAgent?.sessionId) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionId; bounded = { ...bounded, failedAgent }; continue; }
1084
1120
  if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } }; continue; }
1085
1121
  if (Buffer.byteLength(bounded.error.message) > 256) { bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } }; continue; }
1086
1122
  if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) { bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) }; continue; }
@@ -1120,8 +1156,8 @@ async function createWorkflowFailureDiagnostics(store: RunStore, metadata: Workf
1120
1156
  ...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
1121
1157
  structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
1122
1158
  attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
1123
- ...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
1124
- ...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
1159
+ ...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
1160
+ ...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
1125
1161
  } satisfies WorkflowFailureAgent : undefined;
1126
1162
  const completedSiblingAgents = run.agents.filter((agent) => {
1127
1163
  if (agent.state !== "completed" || agent.id === failedAgentRecord?.id) return false;
@@ -1155,7 +1191,7 @@ async function createWorkflowFailureDiagnostics(store: RunStore, metadata: Workf
1155
1191
  }
1156
1192
 
1157
1193
  export function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string {
1158
- 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.sessionFile ? ` session=${diagnostic.failedAgent.sessionFile}` : ""}` : "(not persisted)";
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)";
1159
1195
  const siblingAgents = diagnostic.completedSiblingAgents;
1160
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)";
1161
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}`] : [];
@@ -1538,7 +1574,7 @@ function workflowKeyLabel(keybindings: unknown, binding: string, fallback: strin
1538
1574
  return [...new Set(vim ? [...configured, vim] : configured)].join("/");
1539
1575
  }
1540
1576
 
1541
- export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, createSession: SessionFactory = createNativeAgentSession, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
1577
+ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, transport: AgentTransport = localAgentTransport, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
1542
1578
  beginWorkflowExtensionLoading();
1543
1579
  const registry = loadingRegistry();
1544
1580
  const extensionAgentDir = agentDir ?? getAgentDir();
@@ -1586,6 +1622,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1586
1622
  const foregroundDeliveries = new Map<string, { store: RunStore; inline: boolean; timer?: ReturnType<typeof setTimeout> }>();
1587
1623
  const liveActivities = new Map<string, Map<string, AgentActivity>>();
1588
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); };
1589
1627
  const setLiveActivity = (runId: string, agentId: string, activity?: AgentActivity) => {
1590
1628
  const activities = liveActivities.get(runId);
1591
1629
  if (activity) {
@@ -1746,18 +1784,19 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1746
1784
  const onProgress = async (progress: AgentProgress) => {
1747
1785
  let runState: PersistedRun;
1748
1786
  if (progress.persist) {
1749
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
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);
1750
1788
  } else {
1751
1789
  const loaded = await run.store.load();
1752
1790
  if (!loaded.run.agents.some((agent) => agent.id === id)) return;
1753
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
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) };
1754
1792
  }
1755
1793
  if (!runState.agents.some((agent) => agent.id === id)) return;
1756
1794
  setLiveActivity(runId, id, progress.activity);
1757
1795
  setLiveEventTime(runId, id, progress.lastEventAt);
1758
1796
  run.update?.(workflowToolUpdate(withLiveActivities(runState)));
1759
1797
  };
1760
- const onAttempt = async (attempt: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">) => {
1798
+ const onAttempt = async (attempt: AgentAttempt) => {
1799
+ setLiveAgentSession(runId, id, attempt.liveSession);
1761
1800
  await scheduler.flush();
1762
1801
  scheduler.attemptStarted(id);
1763
1802
  const lastEventAt = Date.now();
@@ -1777,9 +1816,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1777
1816
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
1778
1817
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
1779
1818
  setLiveActivity(runId, id);
1819
+ setLiveAgentSession(runId, id);
1780
1820
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
1781
1821
  return result.value;
1782
1822
  } catch (error) {
1823
+ setLiveAgentSession(runId, id);
1783
1824
  const attempts = (error as WorkflowError & { attempts?: readonly AgentAttempt[] }).attempts;
1784
1825
  if (attempts?.length) {
1785
1826
  const before = (await run.store.load()).run;
@@ -1808,7 +1849,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1808
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 }; }
1809
1850
  const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
1810
1851
  const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
1811
- return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
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 }) };
1812
1853
  });
1813
1854
  return { ...current, agents };
1814
1855
  });
@@ -1826,6 +1867,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1826
1867
  run.checkpointResolvers.clear();
1827
1868
  liveActivities.delete(runId);
1828
1869
  liveEventTimes.delete(runId);
1870
+ for (const key of liveAgentSessions.keys()) if (key.startsWith(`${runId}:`)) liveAgentSessions.delete(key);
1829
1871
  eventPublisher.removeRun(runId);
1830
1872
  runs.delete(runId);
1831
1873
  };
@@ -1961,7 +2003,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1961
2003
  pi.registerTool({
1962
2004
  name: "workflow_catalog",
1963
2005
  label: "Workflow Catalog",
1964
- description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
2006
+ description: "List reusable workflow functions, variables, and model aliases; pass `name` to load one entry in full",
1965
2007
  parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
1966
2008
  async execute(_id, params = {}) {
1967
2009
  const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
@@ -1978,7 +2020,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
1978
2020
  });
1979
2021
  catalogRegistered = true;
1980
2022
  };
1981
- const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
2023
+ const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, transport);
1982
2024
  const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
1983
2025
  ? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
1984
2026
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
@@ -2251,7 +2293,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2251
2293
  const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
2252
2294
  const childInitialBudget = childBudget.snapshot();
2253
2295
  const retry: WorkflowRetryProvenance = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
2254
- await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
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);
2255
2297
  const fallbackModel: ModelSpec = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
2256
2298
  const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
2257
2299
  const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
@@ -2432,7 +2474,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2432
2474
  } : undefined;
2433
2475
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
2434
2476
  const initialBudget = budgetRuntime.snapshot();
2435
- await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
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);
2436
2478
  if (params.foreground) foregroundDeliveries.set(toolCallId, { store, inline: false });
2437
2479
  const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
2438
2480
  const background = !params.foreground;
@@ -2511,7 +2553,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2511
2553
  const details = result.details;
2512
2554
  if (isWorkflowFailureDiagnostics(details)) return textBlock(formatWorkflowFailureDiagnostics(details));
2513
2555
  const runDetails = details as { run?: PersistedRun; value?: JsonValue; preview?: string } | undefined;
2514
- const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval> };
2556
+ const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval>; workflowProgress?: WorkflowProgressRefreshState };
2515
2557
  if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
2516
2558
  state.workflowSpinner = setInterval(context.invalidate, 80);
2517
2559
  state.workflowSpinner.unref();
@@ -2519,7 +2561,31 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2519
2561
  clearInterval(state.workflowSpinner);
2520
2562
  delete state.workflowSpinner;
2521
2563
  }
2522
- if (runDetails?.run) return workflowProgressBlock(runDetails.run, theme);
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
+ }
2523
2589
  const content = result.content[0];
2524
2590
  return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
2525
2591
  },
@@ -2779,33 +2845,32 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2779
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 };
2780
2846
  };
2781
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
+ };
2782
2863
  const agentActionLabels = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): string[] => {
2783
2864
  const worktree = agentWorktreeFor(dashboard, agent);
2784
2865
  return [
2785
- ...(agent.attemptDetails?.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
2866
+ ...visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
2786
2867
  ...(worktree ? ["Copy branch", "Copy worktree path"] : []),
2868
+ ...(ctx.mode === "tui" && agent.prompt !== undefined ? ["Open prompt in editor"] : []),
2787
2869
  ...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
2788
2870
  "Copy agent ID",
2789
2871
  "Back",
2790
2872
  ];
2791
2873
  };
2792
- const forkAgentSession = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): Promise<void> => {
2793
- const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
2794
- const worktree = agentWorktreeFor(dashboard, agent);
2795
- const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
2796
- const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
2797
- const index = choice ? choices.indexOf(choice) : -1;
2798
- const attempt = index >= 0 ? attempts[index] : undefined;
2799
- if (!attempt) return;
2800
- const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
2801
- 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;
2802
- try {
2803
- await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
2804
- ctx.ui.notify("Forked Pi session in pane.", "info");
2805
- } catch (error) {
2806
- ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
2807
- }
2808
- };
2809
2874
  const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, requestedAgentId?: string): Promise<void> => {
2810
2875
  const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
2811
2876
  const title = (agent: AgentRecord): string => agentBreadcrumb(agent, byId, true);
@@ -2823,10 +2888,15 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2823
2888
  for (;;) {
2824
2889
  const action = await ctx.ui.select(title(selected), actions);
2825
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
+ }
2826
2897
  if (action === "Copy agent ID") { await copyArtifact(selected.id, "agent ID"); continue; }
2827
2898
  if (action === "Copy branch" && worktree) { await copyArtifact(worktree.branch, "branch"); continue; }
2828
2899
  if (action === "Copy worktree path" && worktree) { await copyArtifact(worktree.path, "worktree path"); continue; }
2829
- if (action === "Fork as Pi session in pane") await forkAgentSession(dashboard, selected);
2830
2900
  }
2831
2901
  };
2832
2902
  for (;;) {
@@ -2952,7 +3022,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2952
3022
  selectionNeedsScroll = false;
2953
3023
  }
2954
3024
  dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
2955
- const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
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] ?? "";
2956
3026
  return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
2957
3027
  },
2958
3028
  invalidate() {},
@@ -2962,7 +3032,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2962
3032
  if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
2963
3033
  if (actionMode) {
2964
3034
  const options = actionOptions();
2965
- if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) { actionMode = false; dashboardOffset = 0; tui.requestRender(); return; }
3035
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) { actionMode = false; dashboardOffset = 0; tui.requestRender(); return; }
2966
3036
  if (workflowKeyMatches(keybindings, data, "tui.select.up")) actionIndex = (actionIndex + options.length - 1) % options.length;
2967
3037
  else if (workflowKeyMatches(keybindings, data, "tui.select.down")) actionIndex = (actionIndex + 1) % options.length;
2968
3038
  else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
@@ -2973,14 +3043,24 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
2973
3043
  if (!action || action === "Back") { actionMode = false; dashboardOffset = 0; }
2974
3044
  else if (agent) {
2975
3045
  const worktree = agentWorktreeFor(view, agent);
2976
- if (action === "Open result in editor") {
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") {
2977
3050
  const result = view.agentResults.get(agent.id);
2978
3051
  if (result !== undefined) void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
2979
3052
  }
2980
3053
  else if (action === "Copy agent ID") void copyArtifact(agent.id, "agent ID");
2981
3054
  else if (action === "Copy branch" && worktree) void copyArtifact(worktree.branch, "branch");
2982
3055
  else if (action === "Copy worktree path" && worktree) void copyArtifact(worktree.path, "worktree path");
2983
- else done(`__workflow_fork__:${agent.id}`);
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
+ }
2984
3064
  }
2985
3065
  else if (action === "Open script in editor") void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
2986
3066
  else if (action === "Stop") requestStop();
@@ -3027,12 +3107,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
3027
3107
  if (!actionChoice || actionChoice === "Back") { stores = await loadStores(); break; }
3028
3108
  if (actionChoice === "Agents...") { await selectAgent(view); continue; }
3029
3109
  if (actionChoice.startsWith("__workflow_agent__:")) { await selectAgent(view, actionChoice.slice("__workflow_agent__:".length)); continue; }
3030
- if (actionChoice.startsWith("__workflow_fork__:")) {
3031
- const agentId = actionChoice.slice("__workflow_fork__:".length);
3032
- const agent = view.agents.find((candidate) => candidate.id === agentId);
3033
- if (agent) await forkAgentSession(view, agent);
3034
- continue;
3035
- }
3036
3110
  if (actionChoice === "Refresh") continue;
3037
3111
  const copy = view.copies.get(actionChoice);
3038
3112
  if (copy) { await copyArtifact(copy.value, copy.artifact); continue; }
package/src/index.ts CHANGED
@@ -9,9 +9,9 @@ export * from "./workflow-artifacts.js";
9
9
  export * from "./bundles.js";
10
10
  export { default } from "./host.js";
11
11
  export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
12
- export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, RunSummary, RunSummaryAgent, RunSummaryArtifacts, WorktreeReference } from "./persistence.js";
13
- export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
14
- export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
12
+ export type { AwaitingCheckpoint, CompletedOperation, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, RunSummary, RunSummaryAgent, RunSummaryArtifacts, WorktreeReference } from "./persistence.js";
13
+ export { FairAgentScheduler, WorkflowAgentExecutor, createLocalWorkflowAgentSession, localAgentTransport } from "./agent-execution.js";
14
+ export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./agent-execution.js";
15
15
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
16
16
  export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
17
17
  export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -10,9 +10,9 @@ import type { OwnershipRecord } from "./agent-execution.js";
10
10
  import { WorkflowError } from "./types.js";
11
11
  import { loadLaunchSnapshot } from "./utils.js";
12
12
 
13
- export interface NativeSessionReference { sessionId: string; sessionFile: string }
14
13
  export interface EffectiveSystemPrompt { sessionId: string; attempt: number; turn: number; sha256: string; prompt: string }
15
- export interface PersistedRun extends RunRecord { nativeSessions: readonly NativeSessionReference[] }
14
+ export type PersistedRun = RunRecord;
15
+ type LoadedPersistedRun = PersistedRun;
16
16
  export interface RunSummaryAgent { id: string; name: string; label?: string; state: string; role?: string; attempts: number }
17
17
  export interface RunSummaryArtifacts { runDirectory: string; statePath: string; journalPath: string; snapshotPath: string; workflowPath: string; resultPath: string; summaryPath: string }
18
18
  export interface RunSummary { schemaVersion: 1; runId: string; sessionId: string; workflowName: string; state: RunRecord["state"]; createdAt: string; updatedAt: string; terminalAt?: string; usage: WorkflowBudgetUsage; agents: readonly RunSummaryAgent[]; error?: RunRecord["error"]; failedAt?: string; replayablePaths: readonly string[]; incompletePaths: readonly string[]; artifacts: RunSummaryArtifacts }
@@ -270,10 +270,12 @@ export class RunStore {
270
270
  catch { return false; }
271
271
  }
272
272
 
273
- async load(): Promise<{ run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }> {
273
+ async load(): Promise<{ run: LoadedPersistedRun; snapshot: Readonly<LaunchSnapshot> }> {
274
274
  await this.stateWrite;
275
275
  const run = await json<PersistedRun>(join(this.directory, "state.json"));
276
276
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
277
+ const persisted = run as unknown as Record<string, unknown>;
278
+ if (!Array.isArray(persisted.agentSessions) || Object.hasOwn(persisted, "nativeSessions")) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run uses an unsupported agent session format");
277
279
  return { run, snapshot: loadLaunchSnapshot(await json<LaunchSnapshot>(join(this.directory, "snapshot.json"))) };
278
280
  }
279
281
  async loadSummary(): Promise<RunSummary> {