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/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 {
|
|
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 =
|
|
168
|
+
const name = explicitName ?? registeredName ?? "workflow";
|
|
169
169
|
if (typeof args.script !== "string" || !args.script.trim())
|
|
170
|
-
return `workflow ${name}${registeredName ?
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(`
|
|
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.
|
|
1173
|
-
...(value.failedAgent.
|
|
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?.
|
|
1287
|
-
...(failedAttempt?.
|
|
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.
|
|
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,
|
|
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
|
|
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() },
|
|
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: [],
|
|
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: [],
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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);
|
package/dist/src/index.d.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/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
|
|
15
|
-
|
|
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:
|
|
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 {};
|
package/dist/src/persistence.js
CHANGED
|
@@ -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() {
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -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;
|