pi-extensible-workflows 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +63 -9
- package/dist/src/agent-execution.js +250 -92
- package/dist/src/cli.js +11 -2
- package/dist/src/doctor-cleanup.js +66 -31
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +5 -5
- package/dist/src/host.js +160 -76
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/persistence.d.ts +4 -8
- package/dist/src/persistence.js +3 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.js +14 -22
- package/dist/src/types.d.ts +123 -60
- package/dist/src/validation.js +20 -5
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +213 -90
- package/src/cli.ts +14 -3
- package/src/doctor-cleanup.ts +36 -3
- package/src/execution.ts +27 -15
- package/src/host.ts +136 -62
- package/src/index.ts +3 -3
- package/src/persistence.ts +5 -3
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +13 -22
- package/src/types.ts +45 -27
- package/src/validation.ts +13 -4
- package/src/workflow-artifacts.ts +1 -0
package/src/host.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { Type } from "@earendil-works/pi-ai";
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import { FairAgentScheduler, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
11
11
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
12
12
|
import { acquireSessionLease, listRunIds, RunStore, SessionLease, structuralPath as operationPath } from "./persistence.js";
|
|
13
13
|
import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
@@ -16,8 +16,8 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
|
|
|
16
16
|
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
17
17
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
18
18
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
19
|
-
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
20
|
-
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_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 =
|
|
169
|
-
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ?
|
|
168
|
+
const name = explicitName ?? registeredName ?? "workflow";
|
|
169
|
+
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(`
|
|
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.
|
|
1056
|
-
...(value.failedAgent.
|
|
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?.
|
|
1124
|
-
...(failedAttempt?.
|
|
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.
|
|
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,
|
|
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:
|
|
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
|
|
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() },
|
|
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: [],
|
|
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: [],
|
|
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)
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
13
|
-
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
14
|
-
export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook,
|
|
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/src/persistence.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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> {
|