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/dist/src/host.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { Type } from "@earendil-works/pi-ai";
8
8
  import { Value } from "typebox/value";
9
9
  import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
10
- import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
10
+ import { FairAgentScheduler, WorkflowAgentExecutor, localAgentTransport } from "./agent-execution.js";
11
11
  import { herdrPaneId, openHerdrPane } from "./herdr.js";
12
12
  import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
13
13
  import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
@@ -15,7 +15,7 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
15
15
  import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
16
16
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
17
17
  import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
18
- import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
18
+ import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
19
19
  import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STALL_THRESHOLD_MS, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
20
20
  const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
21
21
  const INTERNAL_WORKFLOW_TOOLS = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
@@ -165,14 +165,14 @@ export class RunLifecycle {
165
165
  export function formatWorkflowPreview(args) {
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";
168
+ const name = explicitName ?? registeredName ?? "workflow";
169
169
  if (typeof args.script !== "string" || !args.script.trim())
170
- return `workflow ${name}${registeredName ? "\nRegistered function" : ""}`;
170
+ return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
171
171
  return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
172
172
  }
173
173
  export const WORKFLOW_TOOL_LABEL = "Workflow";
174
- export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
175
- export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
174
+ export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
175
+ export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline and file-backed launches require a non-empty name; registered function launches may use name as an optional run label and otherwise use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
176
176
  function workflowRecoveryGuidance(action, state) {
177
177
  if (action === "resume") {
178
178
  if (state === "failed")
@@ -196,10 +196,11 @@ function workflowRecoveryGuidance(action, state) {
196
196
  return `Only failed workflow runs can be retried; source is ${state}`;
197
197
  }
198
198
  export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
199
- name: Type.Optional(Type.String({ description: "Required non-empty name for the default inline workflow path; invalid for registered function launches" })),
199
+ name: Type.Optional(Type.String({ description: "Optional run label; required and non-empty for inline or file-backed launches, defaults to the registered function name when omitted" })),
200
200
  description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
201
201
  script: Type.Optional(Type.String({ description: "Immutable inline workflow source; default to a named script that fans out with parallel(...) and awaits results before passing them to a summarizing agent(...)" })),
202
- workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name" })),
202
+ scriptPath: Type.Optional(Type.String({ description: "Path to a JavaScript workflow file, read once at launch and persisted as the inline source" })),
203
+ workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name; name may optionally label the run" })),
203
204
  args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
204
205
  foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
205
206
  concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
@@ -549,12 +550,26 @@ function styleForState(map, state, styles) {
549
550
  function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
550
551
  function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
551
552
  function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
553
+ function formatWorkflowRuntime(durationMs) {
554
+ const seconds = Math.max(0, Math.floor(durationMs / 1000));
555
+ if (seconds < 60)
556
+ return `${String(seconds)}s`;
557
+ const minutes = Math.floor(seconds / 60);
558
+ const remainingSeconds = seconds % 60;
559
+ if (minutes < 60)
560
+ return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
561
+ const hours = Math.floor(minutes / 60);
562
+ const remainingMinutes = minutes % 60;
563
+ return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
564
+ }
552
565
  export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
553
566
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
554
567
  const workflowIcon = runStateGlyph(run.state, spinner);
555
568
  const iconStyle = workflowIconStyle(run.state, styles);
556
569
  const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
557
- const lines = [`${iconStyle(workflowIcon)} ${header}`];
570
+ const state = progressStyleForState(run.state, styles)(`[${run.state}]`);
571
+ const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
572
+ const lines = [`${iconStyle(workflowIcon)} ${header} ${state}${runtime}`];
558
573
  const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
559
574
  lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
560
575
  const activeShells = run.activeShells ?? 0;
@@ -586,6 +601,7 @@ function workflowToolUpdate(run) {
586
601
  return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
587
602
  }
588
603
  const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
604
+ const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
589
605
  function textBlock(text) {
590
606
  return {
591
607
  render(width) {
@@ -815,14 +831,41 @@ function themeWorkflowProgressStyles(theme) {
815
831
  bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
816
832
  };
817
833
  }
818
- function workflowProgressBlock(run, theme) {
834
+ function workflowProgressBlock(run, theme, progress, refresh, invalidate) {
819
835
  const styles = themeWorkflowProgressStyles(theme);
836
+ const currentRun = () => {
837
+ const displayed = progress?.run ?? run;
838
+ if (!progress || displayed.state !== "running")
839
+ return displayed;
840
+ const durationMs = Math.max(displayed.usage?.durationMs ?? 0, progress.runtimeBaseMs + Date.now() - progress.runtimeStartedAt);
841
+ return { ...displayed, usage: { ...budgetUsage(displayed.usage), durationMs } };
842
+ };
820
843
  return {
821
844
  render(width) {
822
845
  const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
823
- return truncateWorkflowProgress(formatWorkflowProgress(run, frame, styles), width);
846
+ return truncateWorkflowProgress(formatWorkflowProgress(currentRun(), frame, styles), width);
847
+ },
848
+ invalidate() {
849
+ const displayed = currentRun();
850
+ if (!progress || !refresh || displayed.state !== "running" || !displayed.agents.some((agent) => agent.state === "running"))
851
+ return;
852
+ const now = Date.now();
853
+ if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
854
+ return;
855
+ progress.lastRefreshAt = now;
856
+ const inputRun = progress.inputRun;
857
+ const pending = refresh().then((next) => {
858
+ if (next && progress.inputRun === inputRun) {
859
+ progress.run = next;
860
+ invalidate?.();
861
+ }
862
+ }).catch(() => undefined);
863
+ progress.refresh = pending;
864
+ void pending.finally(() => {
865
+ if (progress.refresh === pending)
866
+ delete progress.refresh;
867
+ });
824
868
  },
825
- invalidate() { },
826
869
  };
827
870
  }
828
871
  export function formatBudgetStatus(run) {
@@ -1014,7 +1057,7 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.no
1014
1057
  for (const checkpoint of checkpoints)
1015
1058
  lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
1016
1059
  lines.push(`Worktrees: ${String(worktrees.length)}`);
1017
- lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
1060
+ lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
1018
1061
  return lines.join("\n");
1019
1062
  }
1020
1063
  export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
@@ -1077,7 +1120,7 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
1077
1120
  if (!agent)
1078
1121
  return [styles.muted("Agent details are unavailable")];
1079
1122
  const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
1080
- const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
1123
+ const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, ...(agent.systemPrompt === undefined ? [] : [`System prompt: ${agent.systemPrompt}`]), `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
1081
1124
  const error = agent.attemptDetails?.at(-1)?.error;
1082
1125
  if (error)
1083
1126
  result.push(styles.error(`Error: ${error.code}: ${error.message}`));
@@ -1169,8 +1212,8 @@ function boundedWorkflowFailureDiagnostics(value) {
1169
1212
  ...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
1170
1213
  structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
1171
1214
  attempt: value.failedAgent.attempt,
1172
- ...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
1173
- ...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
1215
+ ...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
1216
+ ...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
1174
1217
  } } : {}),
1175
1218
  completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
1176
1219
  id: utf8Prefix(agent.id, 128),
@@ -1199,18 +1242,6 @@ function boundedWorkflowFailureDiagnostics(value) {
1199
1242
  bounded = { ...bounded, retry };
1200
1243
  continue;
1201
1244
  }
1202
- if (bounded.failedAgent?.sessionFile) {
1203
- const failedAgent = { ...bounded.failedAgent };
1204
- delete failedAgent.sessionFile;
1205
- bounded = { ...bounded, failedAgent };
1206
- continue;
1207
- }
1208
- if (bounded.failedAgent?.sessionId) {
1209
- const failedAgent = { ...bounded.failedAgent };
1210
- delete failedAgent.sessionId;
1211
- bounded = { ...bounded, failedAgent };
1212
- continue;
1213
- }
1214
1245
  if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) {
1215
1246
  bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } };
1216
1247
  continue;
@@ -1283,8 +1314,8 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
1283
1314
  ...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
1284
1315
  structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
1285
1316
  attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
1286
- ...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
1287
- ...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
1317
+ ...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
1318
+ ...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
1288
1319
  } : undefined;
1289
1320
  const completedSiblingAgents = run.agents.filter((agent) => {
1290
1321
  if (agent.state !== "completed" || agent.id === failedAgentRecord?.id)
@@ -1321,7 +1352,7 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
1321
1352
  });
1322
1353
  }
1323
1354
  export function formatWorkflowFailureDiagnostics(diagnostic) {
1324
- 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)";
1355
+ const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.session ? ` session=${diagnostic.failedAgent.session.transport}/${diagnostic.failedAgent.session.sessionId}` : ""}` : "(not persisted)";
1325
1356
  const siblingAgents = diagnostic.completedSiblingAgents;
1326
1357
  const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
1327
1358
  const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
@@ -1717,7 +1748,7 @@ function workflowKeyLabel(keybindings, binding, fallback, labels) {
1717
1748
  const vim = WORKFLOW_VIM_KEYS[binding];
1718
1749
  return [...new Set(vim ? [...configured, vim] : configured)].join("/");
1719
1750
  }
1720
- export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir, additionalSkillPaths = []) {
1751
+ export default function workflowExtension(pi, home, clipboard = copyToClipboard, transport = localAgentTransport, agentDir, additionalSkillPaths = []) {
1721
1752
  beginWorkflowExtensionLoading();
1722
1753
  const registry = loadingRegistry();
1723
1754
  const extensionAgentDir = agentDir ?? getAgentDir();
@@ -1773,6 +1804,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1773
1804
  const foregroundDeliveries = new Map();
1774
1805
  const liveActivities = new Map();
1775
1806
  const liveEventTimes = new Map();
1807
+ const liveAgentSessions = new Map();
1808
+ const setLiveAgentSession = (runId, agentId, session) => { const key = `${runId}:${agentId}`; if (session)
1809
+ liveAgentSessions.set(key, session);
1810
+ else
1811
+ liveAgentSessions.delete(key); };
1776
1812
  const setLiveActivity = (runId, agentId, activity) => {
1777
1813
  const activities = liveActivities.get(runId);
1778
1814
  if (activity) {
@@ -1980,13 +2016,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1980
2016
  const onProgress = async (progress) => {
1981
2017
  let runState;
1982
2018
  if (progress.persist) {
1983
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
2019
+ runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
1984
2020
  }
1985
2021
  else {
1986
2022
  const loaded = await run.store.load();
1987
2023
  if (!loaded.run.agents.some((agent) => agent.id === id))
1988
2024
  return;
1989
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
2025
+ runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
1990
2026
  }
1991
2027
  if (!runState.agents.some((agent) => agent.id === id))
1992
2028
  return;
@@ -1995,6 +2031,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1995
2031
  run.update?.(workflowToolUpdate(withLiveActivities(runState)));
1996
2032
  };
1997
2033
  const onAttempt = async (attempt) => {
2034
+ setLiveAgentSession(runId, id, attempt.liveSession);
1998
2035
  await scheduler.flush();
1999
2036
  scheduler.attemptStarted(id);
2000
2037
  const lastEventAt = Date.now();
@@ -2014,10 +2051,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2014
2051
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
2015
2052
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2016
2053
  setLiveActivity(runId, id);
2054
+ setLiveAgentSession(runId, id);
2017
2055
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2018
2056
  return result.value;
2019
2057
  }
2020
2058
  catch (error) {
2059
+ setLiveAgentSession(runId, id);
2021
2060
  const attempts = error.attempts;
2022
2061
  if (attempts?.length) {
2023
2062
  const before = (await run.store.load()).run;
@@ -2051,7 +2090,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2051
2090
  }
2052
2091
  const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
2053
2092
  const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
2054
- return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
2093
+ return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
2055
2094
  });
2056
2095
  return { ...current, agents };
2057
2096
  });
@@ -2071,6 +2110,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2071
2110
  run.checkpointResolvers.clear();
2072
2111
  liveActivities.delete(runId);
2073
2112
  liveEventTimes.delete(runId);
2113
+ for (const key of liveAgentSessions.keys())
2114
+ if (key.startsWith(`${runId}:`))
2115
+ liveAgentSessions.delete(key);
2074
2116
  eventPublisher.removeRun(runId);
2075
2117
  runs.delete(runId);
2076
2118
  };
@@ -2232,7 +2274,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2232
2274
  pi.registerTool({
2233
2275
  name: "workflow_catalog",
2234
2276
  label: "Workflow Catalog",
2235
- description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
2277
+ description: "List reusable workflow functions, variables, and model aliases; pass `name` to load one entry in full",
2236
2278
  parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
2237
2279
  async execute(_id, params = {}) {
2238
2280
  const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
@@ -2249,7 +2291,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2249
2291
  });
2250
2292
  catalogRegistered = true;
2251
2293
  };
2252
- const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
2294
+ const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, transport);
2253
2295
  const activeSnapshotTools = (tools, active) => active === "session"
2254
2296
  ? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
2255
2297
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
@@ -2580,7 +2622,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2580
2622
  const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
2581
2623
  const childInitialBudget = childBudget.snapshot();
2582
2624
  const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
2583
- 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);
2625
+ await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
2584
2626
  const fallbackModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
2585
2627
  const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
2586
2628
  const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
@@ -2810,7 +2852,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2810
2852
  } : undefined;
2811
2853
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
2812
2854
  const initialBudget = budgetRuntime.snapshot();
2813
- await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
2855
+ await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
2814
2856
  if (params.foreground)
2815
2857
  foregroundDeliveries.set(toolCallId, { store, inline: false });
2816
2858
  const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
@@ -2910,8 +2952,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2910
2952
  clearInterval(state.workflowSpinner);
2911
2953
  delete state.workflowSpinner;
2912
2954
  }
2913
- if (runDetails?.run)
2914
- return workflowProgressBlock(runDetails.run, theme);
2955
+ if (runDetails?.run) {
2956
+ const incoming = runDetails.run;
2957
+ let progress = state.workflowProgress;
2958
+ if (!isPartial || !progress || progress.runId !== incoming.id) {
2959
+ progress = undefined;
2960
+ delete state.workflowProgress;
2961
+ if (isPartial) {
2962
+ progress = { runId: incoming.id, inputRun: incoming, run: incoming, lastRefreshAt: 0, runtimeStartedAt: Date.now(), runtimeBaseMs: incoming.usage?.durationMs ?? 0 };
2963
+ state.workflowProgress = progress;
2964
+ }
2965
+ }
2966
+ else if (progress.inputRun !== incoming) {
2967
+ if (progress.run.state !== "running" && incoming.state === "running") {
2968
+ progress.runtimeBaseMs = incoming.usage?.durationMs ?? 0;
2969
+ progress.runtimeStartedAt = Date.now();
2970
+ }
2971
+ progress.inputRun = incoming;
2972
+ progress.run = incoming;
2973
+ }
2974
+ return workflowProgressBlock(progress?.run ?? incoming, theme, progress, async () => {
2975
+ const active = runs.get(incoming.id);
2976
+ const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
2977
+ const loaded = await store.load();
2978
+ return withLiveActivities(loaded.run);
2979
+ }, () => { if (state.workflowProgress === progress)
2980
+ context.invalidate(); });
2981
+ }
2915
2982
  const content = result.content[0];
2916
2983
  return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
2917
2984
  },
@@ -3284,36 +3351,39 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3284
3351
  return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
3285
3352
  };
3286
3353
  const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
3354
+ const agentAttemptActionContext = (dashboard, agent) => {
3355
+ const attempt = (agent.attemptDetails ?? []).reduce((latest, candidate) => !latest || candidate.attempt > latest.attempt ? candidate : latest, undefined);
3356
+ if (!attempt)
3357
+ return undefined;
3358
+ const liveCandidate = liveAgentSessions.get(`${dashboard.run.id}:${agent.id}`);
3359
+ const live = liveCandidate && attempt.session && liveCandidate.reference.transport === attempt.session.transport && liveCandidate.reference.sessionId === attempt.session.sessionId ? liveCandidate : undefined;
3360
+ const run = runs.get(dashboard.run.id);
3361
+ const ui = { notify: (message, level = "info") => { ctx.ui.notify(message, level); }, confirm: (title, message) => ctx.ui.confirm(title, message), select: (title, options) => { return ctx.ui.select(title, [...options]); }, input: (title, placeholder) => ctx.ui.input(title, placeholder) };
3362
+ const attemptSnapshot = deepFreeze(structuredClone(attempt));
3363
+ return { run: deepFreeze(structuredClone(dashboard.run)), agent: deepFreeze(structuredClone(agent)), attempt: attemptSnapshot, ...(attemptSnapshot.session ? { session: attemptSnapshot.session } : {}), ...(live ? { liveSession: live } : {}), signal: run?.abortController.signal ?? new AbortController().signal, ui: Object.freeze(ui) };
3364
+ };
3365
+ const visibleAgentAttemptActions = (dashboard, agent) => {
3366
+ const context = agentAttemptActionContext(dashboard, agent);
3367
+ if (!context)
3368
+ return [];
3369
+ return Object.entries(registry.agentAttemptActions()).filter(([, action]) => { try {
3370
+ return action.visible(context);
3371
+ }
3372
+ catch {
3373
+ return false;
3374
+ } });
3375
+ };
3287
3376
  const agentActionLabels = (dashboard, agent) => {
3288
3377
  const worktree = agentWorktreeFor(dashboard, agent);
3289
3378
  return [
3290
- ...(agent.attemptDetails?.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
3379
+ ...visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
3291
3380
  ...(worktree ? ["Copy branch", "Copy worktree path"] : []),
3381
+ ...(ctx.mode === "tui" && agent.prompt !== undefined ? ["Open prompt in editor"] : []),
3292
3382
  ...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
3293
3383
  "Copy agent ID",
3294
3384
  "Back",
3295
3385
  ];
3296
3386
  };
3297
- const forkAgentSession = async (dashboard, agent) => {
3298
- const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
3299
- const worktree = agentWorktreeFor(dashboard, agent);
3300
- const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
3301
- const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
3302
- const index = choice ? choices.indexOf(choice) : -1;
3303
- const attempt = index >= 0 ? attempts[index] : undefined;
3304
- if (!attempt)
3305
- return;
3306
- const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
3307
- 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?"))
3308
- return;
3309
- try {
3310
- await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
3311
- ctx.ui.notify("Forked Pi session in pane.", "info");
3312
- }
3313
- catch (error) {
3314
- ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
3315
- }
3316
- };
3317
3387
  const selectAgent = async (dashboard, requestedAgentId) => {
3318
3388
  const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
3319
3389
  const title = (agent) => agentBreadcrumb(agent, byId, true);
@@ -3334,6 +3404,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3334
3404
  const action = await ctx.ui.select(title(selected), actions);
3335
3405
  if (!action || action === "Back")
3336
3406
  return;
3407
+ const extensionAction = visibleAgentAttemptActions(dashboard, selected).find(([, candidate]) => candidate.label === action);
3408
+ if (extensionAction) {
3409
+ const context = agentAttemptActionContext(dashboard, selected);
3410
+ if (context) {
3411
+ try {
3412
+ await extensionAction[1].run(context);
3413
+ }
3414
+ catch (error) {
3415
+ ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error");
3416
+ }
3417
+ }
3418
+ return;
3419
+ }
3337
3420
  if (action === "Copy agent ID") {
3338
3421
  await copyArtifact(selected.id, "agent ID");
3339
3422
  continue;
@@ -3346,8 +3429,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3346
3429
  await copyArtifact(worktree.path, "worktree path");
3347
3430
  continue;
3348
3431
  }
3349
- if (action === "Fork as Pi session in pane")
3350
- await forkAgentSession(dashboard, selected);
3351
3432
  }
3352
3433
  };
3353
3434
  for (;;) {
@@ -3491,7 +3572,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3491
3572
  selectionNeedsScroll = false;
3492
3573
  }
3493
3574
  dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
3494
- const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3575
+ const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3495
3576
  return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
3496
3577
  },
3497
3578
  invalidate() { },
@@ -3508,7 +3589,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3508
3589
  }
3509
3590
  if (actionMode) {
3510
3591
  const options = actionOptions();
3511
- if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
3592
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
3512
3593
  actionMode = false;
3513
3594
  dashboardOffset = 0;
3514
3595
  tui.requestRender();
@@ -3531,7 +3612,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3531
3612
  }
3532
3613
  else if (agent) {
3533
3614
  const worktree = agentWorktreeFor(view, agent);
3534
- if (action === "Open result in editor") {
3615
+ if (action === "Open prompt in editor") {
3616
+ if (agent.prompt !== undefined)
3617
+ void openArtifact(Promise.resolve(workflowPromptArtifact(agent.prompt)), "agent prompt");
3618
+ }
3619
+ else if (action === "Open result in editor") {
3535
3620
  const result = view.agentResults.get(agent.id);
3536
3621
  if (result !== undefined)
3537
3622
  void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
@@ -3542,8 +3627,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3542
3627
  void copyArtifact(worktree.branch, "branch");
3543
3628
  else if (action === "Copy worktree path" && worktree)
3544
3629
  void copyArtifact(worktree.path, "worktree path");
3545
- else
3546
- done(`__workflow_fork__:${agent.id}`);
3630
+ else {
3631
+ const extensionAction = visibleAgentAttemptActions(view, agent).find(([, candidate]) => candidate.label === action);
3632
+ const actionContext = extensionAction ? agentAttemptActionContext(view, agent) : undefined;
3633
+ if (extensionAction && actionContext) {
3634
+ actionMode = false;
3635
+ void Promise.resolve(extensionAction[1].run(actionContext)).catch((error) => { ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error"); }).finally(() => { void updateDashboard(); });
3636
+ }
3637
+ }
3547
3638
  }
3548
3639
  else if (action === "Open script in editor")
3549
3640
  void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
@@ -3640,13 +3731,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3640
3731
  await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
3641
3732
  continue;
3642
3733
  }
3643
- if (actionChoice.startsWith("__workflow_fork__:")) {
3644
- const agentId = actionChoice.slice("__workflow_fork__:".length);
3645
- const agent = view.agents.find((candidate) => candidate.id === agentId);
3646
- if (agent)
3647
- await forkAgentSession(view, agent);
3648
- continue;
3649
- }
3650
3734
  if (actionChoice === "Refresh")
3651
3735
  continue;
3652
3736
  const copy = view.copies.get(actionChoice);
@@ -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";
package/dist/src/index.js CHANGED
@@ -9,6 +9,6 @@ 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 { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
12
+ export { FairAgentScheduler, WorkflowAgentExecutor, createLocalWorkflowAgentSession, localAgentTransport } from "./agent-execution.js";
13
13
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
14
14
  export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -1,9 +1,5 @@
1
1
  import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowBudgetUsage, WorkflowRunEvent } from "./types.js";
2
2
  import type { OwnershipRecord } from "./agent-execution.js";
3
- export interface NativeSessionReference {
4
- sessionId: string;
5
- sessionFile: string;
6
- }
7
3
  export interface EffectiveSystemPrompt {
8
4
  sessionId: string;
9
5
  attempt: number;
@@ -11,9 +7,8 @@ export interface EffectiveSystemPrompt {
11
7
  sha256: string;
12
8
  prompt: string;
13
9
  }
14
- export interface PersistedRun extends RunRecord {
15
- nativeSessions: readonly NativeSessionReference[];
16
- }
10
+ export type PersistedRun = RunRecord;
11
+ type LoadedPersistedRun = PersistedRun;
17
12
  export interface RunSummaryAgent {
18
13
  id: string;
19
14
  name: string;
@@ -110,7 +105,7 @@ export declare class RunStore {
110
105
  private refreshSummaryBestEffort;
111
106
  isComplete(): Promise<boolean>;
112
107
  load(): Promise<{
113
- run: PersistedRun;
108
+ run: LoadedPersistedRun;
114
109
  snapshot: Readonly<LaunchSnapshot>;
115
110
  }>;
116
111
  loadSummary(): Promise<RunSummary>;
@@ -170,3 +165,4 @@ export declare class RunStore {
170
165
  saveResult(value: JsonValue): Promise<string>;
171
166
  delete(confirmed: boolean): Promise<void>;
172
167
  }
168
+ export {};
@@ -321,6 +321,9 @@ export class RunStore {
321
321
  const run = await json(join(this.directory, "state.json"));
322
322
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId)
323
323
  throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
324
+ const persisted = run;
325
+ if (!Array.isArray(persisted.agentSessions) || Object.hasOwn(persisted, "nativeSessions"))
326
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run uses an unsupported agent session format");
324
327
  return { run, snapshot: loadLaunchSnapshot(await json(join(this.directory, "snapshot.json"))) };
325
328
  }
326
329
  async loadSummary() {
@@ -1,4 +1,4 @@
1
- import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
1
+ import type { AgentAttemptAction, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
2
2
  export declare class WorkflowRegistry {
3
3
  #private;
4
4
  get frozen(): boolean;
@@ -18,6 +18,7 @@ export declare class WorkflowRegistry {
18
18
  variable: WorkflowVariable;
19
19
  }[];
20
20
  agentSetupHooks(): readonly RegisteredAgentSetupHook[];
21
+ agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>>;
21
22
  roleDirectories(): readonly string[];
22
23
  roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
23
24
  modelAliases(): readonly {
@@ -29,7 +30,7 @@ export declare class WorkflowRegistry {
29
30
  }[];
30
31
  resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
31
32
  }
32
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
33
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "agentAttemptActions" | "roleDirectories" | "roleDirectoryRegistrations">;
33
34
  export declare function resetWorkflowRegistry(): void;
34
35
  export declare function beginWorkflowExtensionLoading(): void;
35
36
  export declare function loadingRegistry(): WorkflowRegistryApi;