pi-extensible-workflows 3.2.0 → 3.3.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 +1 -30
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/host.d.ts +9 -6
- package/dist/src/host.js +375 -129
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +8 -2
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +2 -2
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/host.ts +329 -124
- package/src/index.ts +3 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +6 -2
- package/src/workflow-evals.ts +24 -3
package/src/host.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentR
|
|
|
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
19
|
import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
20
|
-
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError, type AgentOptions, type AgentRecord, type AgentResourcePolicy, type BudgetApprovalRequest, type BudgetEvent, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowBridge, type WorkflowCatalogFunction, type WorkflowCatalogIndex, type WorkflowCatalogVariable, type WorkflowCheckpointState, type WorkflowErrorCode, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowFunctionContext, type WorkflowExecution, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowRetryProvenance, type WorkflowRunContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowSiblingAgent, type WorkflowWorktreeReference } from "./types.js";
|
|
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";
|
|
21
21
|
const SETTLED_AGENT_STATES: ReadonlySet<import("./types.js").AgentState> = new Set(["completed", "failed", "cancelled"]);
|
|
22
22
|
const INTERNAL_WORKFLOW_TOOLS: readonly string[] = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
|
|
23
23
|
const HARD_TERMINAL_RUN_STATES: ReadonlySet<string> = new Set(["completed", "failed", "stopped"]);
|
|
@@ -96,6 +96,8 @@ function mainAgentError(error: unknown): WorkflowError {
|
|
|
96
96
|
Object.assign(presented, typed);
|
|
97
97
|
return presented;
|
|
98
98
|
}
|
|
99
|
+
function workflowFailedAt(error: unknown): string | undefined { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
|
|
100
|
+
function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
99
101
|
|
|
100
102
|
export class RunLifecycle {
|
|
101
103
|
#state: RunState;
|
|
@@ -169,7 +171,7 @@ export function formatWorkflowPreview(args: { script?: unknown; workflow?: unkno
|
|
|
169
171
|
}
|
|
170
172
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
171
173
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
172
|
-
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Foreground results include the completed run ID. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId }) replays a failed run into a child; workflow_resume({ runId, budget? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes."
|
|
174
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes."
|
|
173
175
|
function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState): string {
|
|
174
176
|
if (action === "resume") {
|
|
175
177
|
if (state === "failed") return "Failed workflow runs must use workflow_retry({ runId })";
|
|
@@ -195,7 +197,7 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
195
197
|
budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
|
|
196
198
|
parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
|
|
197
199
|
});
|
|
198
|
-
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
200
|
+
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
199
201
|
|
|
200
202
|
type WorkflowToolUpdate = { content: [{ type: "text"; text: string }]; details: { runId: string; run: PersistedRun } };
|
|
201
203
|
export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
|
|
@@ -482,7 +484,7 @@ function styleForState(map: Record<string, ProgressStyleKey>, state: string, sty
|
|
|
482
484
|
function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
483
485
|
function workflowIconStyle(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
484
486
|
function phaseStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
485
|
-
export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
487
|
+
export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string {
|
|
486
488
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
487
489
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
488
490
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
@@ -490,11 +492,13 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
490
492
|
const lines = [`${iconStyle(workflowIcon)} ${header}`];
|
|
491
493
|
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
492
494
|
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
495
|
+
const activeShells = run.activeShells ?? 0;
|
|
496
|
+
if (activeShells > 0) lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
|
|
493
497
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
494
498
|
const renderAgents = (agents: readonly AgentRecord[], offset: number, nested: boolean) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
|
|
495
499
|
const icon = agentStateGlyph(agent.state, spinner);
|
|
496
500
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
497
|
-
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
|
|
501
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
498
502
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
499
503
|
const state = progressStyleForState(agent.state, styles);
|
|
500
504
|
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
|
|
@@ -560,7 +564,7 @@ function controlState(state: string, theme: Theme): string {
|
|
|
560
564
|
return theme.fg(color, state);
|
|
561
565
|
}
|
|
562
566
|
function controlAction(action: string, theme: Theme): string {
|
|
563
|
-
const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
567
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
564
568
|
return theme.fg(color, action);
|
|
565
569
|
}
|
|
566
570
|
function budgetPatchEntries(value: unknown): string[] {
|
|
@@ -611,13 +615,14 @@ function workflowControlResult(name: string, args: Record<string, unknown>, resu
|
|
|
611
615
|
if (name === "workflow_retry") {
|
|
612
616
|
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
613
617
|
const state = controlString(value.state) ?? "unknown";
|
|
614
|
-
|
|
615
|
-
return [title, `Source
|
|
618
|
+
const action = state === "completed" ? "completed" : "started";
|
|
619
|
+
if (!expanded) return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
620
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
|
|
616
621
|
}
|
|
617
622
|
if (name === "workflow_resume") {
|
|
618
623
|
const state = controlString(value.state) ?? "unknown";
|
|
619
624
|
const proposalId = controlString(value.proposalId);
|
|
620
|
-
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
|
|
625
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
621
626
|
if (!expanded) return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
622
627
|
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal: ${theme.fg("accent", proposalId)}`] : []), ...budgetPatchDetails(args.budget, theme)].join("\n");
|
|
623
628
|
}
|
|
@@ -809,18 +814,42 @@ function styledAgentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord
|
|
|
809
814
|
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
810
815
|
}
|
|
811
816
|
|
|
812
|
-
function
|
|
817
|
+
export function formatStalledDuration(durationMs: number): string {
|
|
818
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
819
|
+
if (minutes < 60) return `${String(minutes)}m`;
|
|
820
|
+
const hours = Math.floor(minutes / 60);
|
|
821
|
+
const remainingMinutes = minutes % 60;
|
|
822
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
823
|
+
}
|
|
824
|
+
function stalledDuration(agent: AgentRecord, now: number): number | undefined {
|
|
825
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt)) return undefined;
|
|
826
|
+
const duration = now - agent.lastEventAt;
|
|
827
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
828
|
+
}
|
|
829
|
+
function formatAgentActivity(agent: AgentRecord, spinner: string, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string {
|
|
813
830
|
const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
|
|
814
|
-
|
|
831
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
832
|
+
const stalled = stalledDuration(agent, now);
|
|
833
|
+
if (stalled === undefined) return activity;
|
|
834
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
835
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function formatAccountingValue(value: number): string {
|
|
839
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
815
840
|
}
|
|
816
841
|
|
|
817
842
|
function formatAccounting(accounting: NonNullable<AgentRecord["accounting"]>): string {
|
|
818
843
|
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
819
|
-
return `${
|
|
844
|
+
return `${formatAccountingValue(total)} tok`;
|
|
820
845
|
}
|
|
821
846
|
|
|
847
|
+
function formatAgentAccounting(accounting: NonNullable<AgentRecord["accounting"]>): string[] {
|
|
848
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
849
|
+
return [`Tokens: ${formatAccountingValue(total)} (in=${formatAccountingValue(accounting.input)} out=${formatAccountingValue(accounting.output)} cache-read=${formatAccountingValue(accounting.cacheRead)} cache-write=${formatAccountingValue(accounting.cacheWrite)})`, `Cost: $${accounting.cost.toFixed(2)}`];
|
|
850
|
+
}
|
|
822
851
|
|
|
823
|
-
export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string {
|
|
852
|
+
export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now = Date.now()): string {
|
|
824
853
|
void worktrees;
|
|
825
854
|
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
826
855
|
const totalAccounting = run.agents.reduce((sum, a) => ({ input: sum.input + (a.accounting?.input ?? 0), output: sum.output + (a.accounting?.output ?? 0), cacheRead: sum.cacheRead + (a.accounting?.cacheRead ?? 0), cacheWrite: sum.cacheWrite + (a.accounting?.cacheWrite ?? 0), cost: sum.cost + (a.accounting?.cost ?? 0) }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
@@ -843,7 +872,7 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
843
872
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
844
873
|
if (last?.error) result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
845
874
|
}
|
|
846
|
-
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
|
|
875
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
847
876
|
if (activity) result.push(`${indent} ${activity}`);
|
|
848
877
|
return result.join("\n");
|
|
849
878
|
};
|
|
@@ -852,7 +881,7 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
852
881
|
return lines.join("\n");
|
|
853
882
|
}
|
|
854
883
|
|
|
855
|
-
export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string {
|
|
884
|
+
export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now = Date.now()): string {
|
|
856
885
|
const { run, snapshot } = loaded;
|
|
857
886
|
const lines = [
|
|
858
887
|
`Workflow: ${run.workflowName}`,
|
|
@@ -881,6 +910,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
881
910
|
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${role}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
|
|
882
911
|
for (const attempt of agent.attemptDetails ?? []) result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
883
912
|
for (const call of agent.toolCalls ?? []) result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
913
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
914
|
+
if (activity) result.push(`${indent} ${activity}`);
|
|
884
915
|
return result.join("\n");
|
|
885
916
|
}));
|
|
886
917
|
lines.push("Checkpoints:");
|
|
@@ -890,7 +921,7 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
890
921
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
891
922
|
return lines.join("\n");
|
|
892
923
|
}
|
|
893
|
-
export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string[] {
|
|
924
|
+
export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()): string[] {
|
|
894
925
|
const safeWidth = Math.max(1, width);
|
|
895
926
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
896
927
|
const tree = buildWorkflowPhaseTree(model);
|
|
@@ -921,7 +952,8 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
921
952
|
const treeLine = (node: WorkflowPhaseTreeNode): string => {
|
|
922
953
|
const selected = node.id === selectedNode?.id;
|
|
923
954
|
const state = progressStyleForState(node.state, styles);
|
|
924
|
-
|
|
955
|
+
const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
|
|
956
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
|
|
925
957
|
};
|
|
926
958
|
const details = (node: WorkflowPhaseTreeNode | undefined): string[] => {
|
|
927
959
|
if (!node) return [styles.muted("No workflow node is selected")];
|
|
@@ -938,10 +970,12 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
938
970
|
const agent = node.agent;
|
|
939
971
|
if (!agent) return [styles.muted("Agent details are unavailable")];
|
|
940
972
|
const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
|
|
941
|
-
const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
|
|
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")])];
|
|
942
974
|
const error = agent.attemptDetails?.at(-1)?.error;
|
|
943
975
|
if (error) result.push(styles.error(`Error: ${error.code}: ${error.message}`));
|
|
944
976
|
if (agent.activity) result.push(`Activity: ${agent.activity.text}`);
|
|
977
|
+
const stalled = stalledDuration(agent, now);
|
|
978
|
+
if (stalled !== undefined) result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
|
|
945
979
|
return result;
|
|
946
980
|
};
|
|
947
981
|
const stateNames: readonly WorkflowPhaseState[] = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
@@ -1127,16 +1161,35 @@ export function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiag
|
|
|
1127
1161
|
const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
|
|
1128
1162
|
return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
|
|
1129
1163
|
}
|
|
1164
|
+
function deliveryPart(value: string, maxBytes: number): string { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
|
|
1165
|
+
export function formatWorkflowFailureDelivery(diagnostic: WorkflowFailureDiagnostics): string {
|
|
1166
|
+
const name = deliveryPart(diagnostic.workflowName, 128);
|
|
1167
|
+
const runId = deliveryPart(diagnostic.runId, 128);
|
|
1168
|
+
const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
|
|
1169
|
+
const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
|
|
1170
|
+
const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
|
|
1171
|
+
const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
|
|
1172
|
+
const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
|
|
1173
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1174
|
+
}
|
|
1175
|
+
function formatWorkflowFailureDeliveryFallback(workflowName: string, runId: string, runDirectory: string, error: unknown): string {
|
|
1176
|
+
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
1177
|
+
const failedPath = workflowFailedAt(error);
|
|
1178
|
+
const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
|
|
1179
|
+
const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
|
|
1180
|
+
return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
|
|
1181
|
+
}
|
|
1130
1182
|
|
|
1131
1183
|
function serializeWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string { return JSON.stringify(diagnostic); }
|
|
1132
1184
|
function isWorkflowFailureDiagnostics(value: unknown): value is WorkflowFailureDiagnostics {
|
|
1133
1185
|
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
1134
1186
|
}
|
|
1135
1187
|
function deliver(pi: ExtensionAPI, content: string): void {
|
|
1188
|
+
if (typeof pi.sendMessage !== "function") return;
|
|
1136
1189
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1137
1190
|
}
|
|
1138
1191
|
function deliverFailure(pi: ExtensionAPI, diagnostic: WorkflowFailureDiagnostics): void {
|
|
1139
|
-
deliver(pi,
|
|
1192
|
+
deliver(pi, formatWorkflowFailureDelivery(diagnostic));
|
|
1140
1193
|
}
|
|
1141
1194
|
|
|
1142
1195
|
type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -1456,6 +1509,7 @@ function tuiRows(tui: unknown): number {
|
|
|
1456
1509
|
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
1457
1510
|
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
1458
1511
|
}
|
|
1512
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
1459
1513
|
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
1460
1514
|
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
1461
1515
|
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } } as const;
|
|
@@ -1474,8 +1528,17 @@ function keybindingKeys(keybindings: unknown, name: string): readonly string[] |
|
|
|
1474
1528
|
const getKeys = object(keybindings) ? asFn(keybindings.getKeys) as NonNullable<KeybindingsHostCapabilities["getKeys"]> | undefined : undefined;
|
|
1475
1529
|
return getKeys ? getKeys.call(keybindings, name) : undefined;
|
|
1476
1530
|
}
|
|
1531
|
+
type WorkflowKeybindings = { matches(data: string, binding: string): boolean };
|
|
1532
|
+
const WORKFLOW_VIM_KEYS: Readonly<Record<string, string>> = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
1533
|
+
function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
1534
|
+
function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels: Readonly<Record<string, string>>): string {
|
|
1535
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
1536
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
1537
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
1538
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
1539
|
+
}
|
|
1477
1540
|
|
|
1478
|
-
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, createSession: SessionFactory = createNativeAgentSession, agentDir?: string) {
|
|
1541
|
+
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, createSession: SessionFactory = createNativeAgentSession, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
|
|
1479
1542
|
beginWorkflowExtensionLoading();
|
|
1480
1543
|
const registry = loadingRegistry();
|
|
1481
1544
|
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
@@ -1496,7 +1559,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1496
1559
|
const skillPath = [join(extensionDir, "../skills"), join(extensionDir, "../../skills")].find((path) => existsSync(path));
|
|
1497
1560
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
1498
1561
|
});
|
|
1499
|
-
type BudgetDecisionResult = { state: "running" | "budget_exhausted"; approved: boolean };
|
|
1562
|
+
type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun };
|
|
1500
1563
|
const runs = new Map<string, { executor: WorkflowAgentExecutor; store: RunStore; metadata: WorkflowMetadata; model: ModelSpec; lifecycle: RunLifecycle; budget: WorkflowBudgetRuntime; abortController: AbortController; projectTrusted: () => boolean; providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>; execution?: WorkflowExecution; completion?: Promise<unknown>; checkpointResolvers: Map<string, (value: boolean) => void>; update?: (result: WorkflowToolUpdate) => void }>();
|
|
1501
1564
|
let providerRecoveryQueue = Promise.resolve();
|
|
1502
1565
|
const enqueueProviderRecovery = <T>(task: () => Promise<T>): Promise<T> => { const next = providerRecoveryQueue.then(task, task); providerRecoveryQueue = next.then(() => undefined, () => undefined); return next; };
|
|
@@ -1520,14 +1583,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1520
1583
|
});
|
|
1521
1584
|
};
|
|
1522
1585
|
const pendingFailureDiagnostics = new Map<string, WorkflowFailureDiagnostics>();
|
|
1523
|
-
|
|
1524
|
-
if (event.toolName !== "workflow" || !event.isError) return;
|
|
1525
|
-
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1526
|
-
if (!diagnostic) return;
|
|
1527
|
-
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1528
|
-
return { content: [{ type: "text" as const, text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1529
|
-
});
|
|
1586
|
+
const foregroundDeliveries = new Map<string, { store: RunStore; inline: boolean; timer?: ReturnType<typeof setTimeout> }>();
|
|
1530
1587
|
const liveActivities = new Map<string, Map<string, AgentActivity>>();
|
|
1588
|
+
const liveEventTimes = new Map<string, Map<string, number>>();
|
|
1531
1589
|
const setLiveActivity = (runId: string, agentId: string, activity?: AgentActivity) => {
|
|
1532
1590
|
const activities = liveActivities.get(runId);
|
|
1533
1591
|
if (activity) {
|
|
@@ -1538,12 +1596,22 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1538
1596
|
if (activities?.size === 0) liveActivities.delete(runId);
|
|
1539
1597
|
}
|
|
1540
1598
|
};
|
|
1599
|
+
const setLiveEventTime = (runId: string, agentId: string, timestamp?: number) => {
|
|
1600
|
+
if (timestamp === undefined) return;
|
|
1601
|
+
const timestamps = liveEventTimes.get(runId);
|
|
1602
|
+
if (timestamps) timestamps.set(agentId, timestamp);
|
|
1603
|
+
else liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
|
|
1604
|
+
};
|
|
1541
1605
|
const withLiveActivities = (run: PersistedRun): PersistedRun => {
|
|
1542
1606
|
const activities = liveActivities.get(run.id);
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1607
|
+
const timestamps = liveEventTimes.get(run.id);
|
|
1608
|
+
if (!activities?.size && !timestamps?.size) return run;
|
|
1609
|
+
return { ...run, agents: run.agents.map((agent) => {
|
|
1610
|
+
const activity = activities?.get(agent.id);
|
|
1611
|
+
const lastEventAt = timestamps?.get(agent.id);
|
|
1612
|
+
if (activity === undefined && lastEventAt === undefined) return agent;
|
|
1613
|
+
return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
1614
|
+
}) };
|
|
1547
1615
|
};
|
|
1548
1616
|
const terminalRunStates = new Map<string, "completed" | "failed" | "stopped">();
|
|
1549
1617
|
let sessionLease: SessionLease | undefined;
|
|
@@ -1565,6 +1633,42 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1565
1633
|
await eventPublisher.budget(store, metadata, persisted);
|
|
1566
1634
|
return persisted;
|
|
1567
1635
|
};
|
|
1636
|
+
pi.on("tool_result", async (event) => {
|
|
1637
|
+
const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
|
|
1638
|
+
if (delivery) {
|
|
1639
|
+
if (delivery.timer) clearTimeout(delivery.timer);
|
|
1640
|
+
delivery.inline = true;
|
|
1641
|
+
await delivery.store.updateState((current) => {
|
|
1642
|
+
if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered") return current;
|
|
1643
|
+
return { ...current, delivery: { ...current.delivery, state: "delivered" } };
|
|
1644
|
+
});
|
|
1645
|
+
foregroundDeliveries.delete(event.toolCallId);
|
|
1646
|
+
}
|
|
1647
|
+
if (event.toolName !== "workflow" || !event.isError) return;
|
|
1648
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
1649
|
+
if (!diagnostic) return;
|
|
1650
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
1651
|
+
return { content: [{ type: "text" as const, text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
1652
|
+
});
|
|
1653
|
+
const deliverTerminal = async (store: RunStore, content: string): Promise<void> => {
|
|
1654
|
+
let claimed: boolean | undefined;
|
|
1655
|
+
await store.updateState((current) => {
|
|
1656
|
+
if (current.delivery?.state === "delivered") return current;
|
|
1657
|
+
if (!current.delivery) { claimed = true; return current; }
|
|
1658
|
+
claimed = true;
|
|
1659
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
|
|
1660
|
+
});
|
|
1661
|
+
if (claimed === true) deliver(pi, content);
|
|
1662
|
+
};
|
|
1663
|
+
const scheduleForegroundDelivery = (toolCallId: string, send: () => Promise<void>): void => {
|
|
1664
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
1665
|
+
if (!delivery || delivery.inline || typeof (pi as unknown as { sendMessage?: unknown }).sendMessage !== "function") return;
|
|
1666
|
+
//NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
|
|
1667
|
+
delivery.timer = setTimeout(() => {
|
|
1668
|
+
delete delivery.timer;
|
|
1669
|
+
void send().finally(() => foregroundDeliveries.delete(toolCallId));
|
|
1670
|
+
}, 0);
|
|
1671
|
+
};
|
|
1568
1672
|
const phaseBridge = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => {
|
|
1569
1673
|
let cursor = 0;
|
|
1570
1674
|
return async (phase: string): Promise<void> => {
|
|
@@ -1605,17 +1709,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1605
1709
|
const path = shellIdentityPath(identity);
|
|
1606
1710
|
const replayed = await store.replay(path);
|
|
1607
1711
|
if (replayed) return readShellResult(replayed.value);
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1712
|
+
const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
|
|
1713
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
|
|
1714
|
+
try {
|
|
1715
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
1716
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
1717
|
+
await store.complete(path, result as unknown as JsonValue);
|
|
1718
|
+
return result;
|
|
1719
|
+
} finally {
|
|
1720
|
+
const stopped = await persistRunState(store, metadata, (current) => {
|
|
1721
|
+
const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
|
|
1722
|
+
if (activeShells > 0) return { ...current, activeShells };
|
|
1723
|
+
const next = { ...current };
|
|
1724
|
+
delete next.activeShells;
|
|
1725
|
+
return next;
|
|
1726
|
+
});
|
|
1727
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
|
|
1728
|
+
}
|
|
1612
1729
|
} finally { await lifecycle.leave(); }
|
|
1613
1730
|
};
|
|
1614
1731
|
const lifecycleFor = (store: RunStore, state: RunState, budget: WorkflowBudgetRuntime, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
1615
1732
|
if (next !== "pausing") budget.transition(next);
|
|
1616
1733
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
1617
1734
|
const nextRun = { ...current, state: next, ...budget.snapshot() };
|
|
1618
|
-
if (next === "running" || next === "completed") delete nextRun.error;
|
|
1735
|
+
if (next === "running" || next === "completed") { delete nextRun.error; delete nextRun.failedAt; }
|
|
1619
1736
|
return nextRun;
|
|
1620
1737
|
});
|
|
1621
1738
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
@@ -1629,25 +1746,28 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1629
1746
|
const onProgress = async (progress: AgentProgress) => {
|
|
1630
1747
|
let runState: PersistedRun;
|
|
1631
1748
|
if (progress.persist) {
|
|
1632
|
-
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) } : current);
|
|
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);
|
|
1633
1750
|
} else {
|
|
1634
1751
|
const loaded = await run.store.load();
|
|
1635
1752
|
if (!loaded.run.agents.some((agent) => agent.id === id)) return;
|
|
1636
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
|
|
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) };
|
|
1637
1754
|
}
|
|
1638
1755
|
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
1639
1756
|
setLiveActivity(runId, id, progress.activity);
|
|
1757
|
+
setLiveEventTime(runId, id, progress.lastEventAt);
|
|
1640
1758
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
1641
1759
|
};
|
|
1642
1760
|
const onAttempt = async (attempt: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">) => {
|
|
1643
1761
|
await scheduler.flush();
|
|
1644
1762
|
scheduler.attemptStarted(id);
|
|
1763
|
+
const lastEventAt = Date.now();
|
|
1764
|
+
setLiveEventTime(runId, id, lastEventAt);
|
|
1645
1765
|
await scheduler.flush();
|
|
1646
1766
|
const before = (await run.store.load()).run;
|
|
1647
1767
|
await persistActiveAgentAttempt(run.store, id, attempt);
|
|
1648
1768
|
const active = (await run.store.load()).run;
|
|
1649
1769
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
1650
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1770
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
1651
1771
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
1652
1772
|
};
|
|
1653
1773
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
@@ -1687,7 +1807,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1687
1807
|
try { effective = run.executor.resolve(requested); }
|
|
1688
1808
|
catch { effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : { ...run.model, ...(node.options.thinking ? { thinking: node.options.thinking } : {}) }, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools }; }
|
|
1689
1809
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
1690
|
-
|
|
1810
|
+
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 }) };
|
|
1691
1812
|
});
|
|
1692
1813
|
return { ...current, agents };
|
|
1693
1814
|
});
|
|
@@ -1704,6 +1825,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1704
1825
|
terminalRunStates.set(runId, run.lifecycle.state as "completed" | "failed" | "stopped");
|
|
1705
1826
|
run.checkpointResolvers.clear();
|
|
1706
1827
|
liveActivities.delete(runId);
|
|
1828
|
+
liveEventTimes.delete(runId);
|
|
1707
1829
|
eventPublisher.removeRun(runId);
|
|
1708
1830
|
runs.delete(runId);
|
|
1709
1831
|
};
|
|
@@ -1739,13 +1861,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1739
1861
|
run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
|
|
1740
1862
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1741
1863
|
};
|
|
1742
|
-
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult | undefined> => {
|
|
1864
|
+
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult | undefined> => {
|
|
1743
1865
|
const run = runs.get(runId);
|
|
1744
1866
|
if (!run) return undefined;
|
|
1745
1867
|
const request = await run.store.answerWorkflowDecision(proposalId, approved);
|
|
1746
1868
|
if (!request) return undefined;
|
|
1747
1869
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1748
|
-
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
1870
|
+
const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
|
|
1749
1871
|
if (!silent) deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1750
1872
|
return result;
|
|
1751
1873
|
};
|
|
@@ -1856,7 +1978,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1856
1978
|
});
|
|
1857
1979
|
catalogRegistered = true;
|
|
1858
1980
|
};
|
|
1859
|
-
const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
1981
|
+
const createAgentExecutor = (root: Omit<import("./agent-execution.js").AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
|
|
1860
1982
|
const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
|
|
1861
1983
|
? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
|
|
1862
1984
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
@@ -1896,7 +2018,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1896
2018
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1897
2019
|
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
1898
2020
|
};
|
|
1899
|
-
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string) => async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: import("./types.js").AgentIdentity) => {
|
|
2021
|
+
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: import("./types.js").AgentIdentity) => {
|
|
1900
2022
|
await lifecycle.enter();
|
|
1901
2023
|
try {
|
|
1902
2024
|
const path = agentIdentityPath(identity);
|
|
@@ -1911,6 +2033,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1911
2033
|
const thinking = parseThinking(options.thinking);
|
|
1912
2034
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1913
2035
|
const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools as string[] } : {}) });
|
|
2036
|
+
if (role) await captureRole?.(role, resolved.model);
|
|
1914
2037
|
const label = displayAgentName(requestedLabel, role, resolved.model);
|
|
1915
2038
|
const tools = resolved.tools;
|
|
1916
2039
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
@@ -1935,8 +2058,22 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1935
2058
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1936
2059
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1937
2060
|
};
|
|
1938
|
-
const
|
|
2061
|
+
const recoveryUi = (context: unknown): { hasUI: boolean; ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> } } => {
|
|
2062
|
+
const host = object(context) ? context : undefined;
|
|
2063
|
+
const ui = host && object(host.ui) ? host.ui as { select?: (prompt: string, options: string[]) => Promise<string | undefined> } : {};
|
|
2064
|
+
return { hasUI: host?.hasUI === true, ui };
|
|
2065
|
+
};
|
|
2066
|
+
type ColdResumeResult = { value: JsonValue; resultPath: string };
|
|
2067
|
+
const coldResumeRun = async (run: NonNullable<ReturnType<typeof runs.get>>, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
1939
2068
|
const loaded = await run.store.load();
|
|
2069
|
+
const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
|
|
2070
|
+
if (loaded.run.activeShells !== undefined) {
|
|
2071
|
+
await persistRunState(run.store, run.metadata, (current) => {
|
|
2072
|
+
const next = { ...current };
|
|
2073
|
+
delete next.activeShells;
|
|
2074
|
+
return next;
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
1940
2077
|
await run.store.validateRetrySource();
|
|
1941
2078
|
await run.store.validateBorrowedWorktrees();
|
|
1942
2079
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
@@ -1950,7 +2087,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1950
2087
|
run.abortController = controller;
|
|
1951
2088
|
const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
|
|
1952
2089
|
if (!script) throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
1953
|
-
|
|
2090
|
+
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
2091
|
+
await run.store.saveSnapshot(persistedSnapshot);
|
|
1954
2092
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1955
2093
|
run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
|
|
1956
2094
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
@@ -1961,13 +2099,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1961
2099
|
try { variables = await resolveWorkflowVariables(runContext, controller, registry); }
|
|
1962
2100
|
catch (error) {
|
|
1963
2101
|
const typed = asWorkflowError(error);
|
|
1964
|
-
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) { await run.lifecycle.terminal("failed", typed.code).catch(() => undefined); const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current
|
|
2102
|
+
if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) { await run.lifecycle.terminal("failed", typed.code).catch(() => undefined); const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed)); await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed"); run.update?.(workflowToolUpdate(persisted)); if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted).then((diagnostic) => { deliverFailure(pi, diagnostic); }).catch(() => undefined); }
|
|
1965
2103
|
await cleanupTerminalRun(run.store.runId);
|
|
1966
2104
|
throw typed;
|
|
1967
2105
|
}
|
|
1968
2106
|
await scheduler.cancelRun(run.store.runId);
|
|
1969
2107
|
await run.lifecycle.resume();
|
|
1970
|
-
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata,
|
|
2108
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
1971
2109
|
run.execution = execution;
|
|
1972
2110
|
const completion = execution.result.then(async (value) => {
|
|
1973
2111
|
await scheduler.flush();
|
|
@@ -1980,17 +2118,28 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1980
2118
|
await scheduler.flush();
|
|
1981
2119
|
const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
|
|
1982
2120
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
1983
|
-
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot()
|
|
2121
|
+
const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
|
|
1984
2122
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
1985
2123
|
if (state === "failed") retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
|
|
1986
2124
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
1987
2125
|
run.update?.(workflowToolUpdate(persisted));
|
|
1988
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted).
|
|
2126
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) { const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted); Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic }); }
|
|
2127
|
+
throw typed;
|
|
1989
2128
|
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
1990
2129
|
run.completion = completion;
|
|
1991
|
-
|
|
2130
|
+
if (!foreground || !waitForCompletion) {
|
|
2131
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2132
|
+
deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
|
|
2133
|
+
}, (error: unknown) => {
|
|
2134
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2135
|
+
if (diagnostic) deliverFailure(pi, diagnostic);
|
|
2136
|
+
else deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
|
|
2137
|
+
});
|
|
2138
|
+
return undefined;
|
|
2139
|
+
}
|
|
2140
|
+
return completion;
|
|
1992
2141
|
};
|
|
1993
|
-
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult> => {
|
|
2142
|
+
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal, waitForCompletion = true): Promise<BudgetDecisionResult> => {
|
|
1994
2143
|
const run = runs.get(request.runId);
|
|
1995
2144
|
if (!run) throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
1996
2145
|
if (!approved) return { state: "budget_exhausted", approved: false };
|
|
@@ -1999,10 +2148,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1999
2148
|
const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
|
|
2000
2149
|
run.budget = runtime;
|
|
2001
2150
|
await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
|
|
2002
|
-
|
|
2151
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2152
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
|
|
2153
|
+
if (completed) return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
|
|
2003
2154
|
return { state: "running", approved: true };
|
|
2004
2155
|
};
|
|
2005
|
-
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal): Promise<Record<string, JsonValue>> => {
|
|
2156
|
+
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal, modeOverride?: boolean, waitForCompletion = true): Promise<Record<string, JsonValue>> => {
|
|
2006
2157
|
const run = runs.get(runId);
|
|
2007
2158
|
if (!run) {
|
|
2008
2159
|
const host = object(context) ? context : {};
|
|
@@ -2041,11 +2192,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2041
2192
|
run.budget = runtime;
|
|
2042
2193
|
await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget) next.budget = nextBudget; else delete next.budget; return next; });
|
|
2043
2194
|
}
|
|
2044
|
-
|
|
2195
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2196
|
+
const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
|
|
2197
|
+
if (completed) return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run as unknown as JsonValue };
|
|
2045
2198
|
return { state: "running" };
|
|
2046
2199
|
};
|
|
2047
2200
|
const retryReservations = new Set<string>();
|
|
2048
|
-
const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal): Promise<{ runId: string; parentRunId: string; state: "running" }> => {
|
|
2201
|
+
const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal, modeOverride?: boolean): Promise<{ runId: string; parentRunId: string; state: "running" | "completed"; value?: JsonValue; run?: PersistedRun }> => {
|
|
2049
2202
|
if (typeof runId !== "string" || !runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
2050
2203
|
const host = object(context) ? context : {};
|
|
2051
2204
|
const cwd = typeof host.cwd === "string" ? host.cwd : undefined;
|
|
@@ -2109,12 +2262,17 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2109
2262
|
runs.set(childRunId, childRun);
|
|
2110
2263
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
2111
2264
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
2112
|
-
|
|
2265
|
+
const { hasUI, ui } = recoveryUi(context);
|
|
2266
|
+
const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
2113
2267
|
const completion = runs.get(childRunId)?.completion;
|
|
2114
2268
|
if (completion) {
|
|
2115
2269
|
childStarted = true;
|
|
2116
2270
|
void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
|
|
2271
|
+
} else if (completed) {
|
|
2272
|
+
childStarted = true;
|
|
2273
|
+
retryReservations.delete(lineageRootRunId);
|
|
2117
2274
|
}
|
|
2275
|
+
if (completed) return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
|
|
2118
2276
|
return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
|
|
2119
2277
|
} finally {
|
|
2120
2278
|
if (!childStarted) retryReservations.delete(lineageRootRunId);
|
|
@@ -2126,7 +2284,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2126
2284
|
description: "Retry a failed workflow run by replaying its completed structural operations",
|
|
2127
2285
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
2128
2286
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2129
|
-
try { const result = await retryWorkflowRun(params.runId, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2287
|
+
try { const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2130
2288
|
catch (error) { throw mainAgentError(error); }
|
|
2131
2289
|
},
|
|
2132
2290
|
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_retry", args, theme)); },
|
|
@@ -2136,9 +2294,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2136
2294
|
name: "workflow_resume",
|
|
2137
2295
|
label: "Workflow Resume",
|
|
2138
2296
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
2139
|
-
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
2297
|
+
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
|
|
2140
2298
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
2141
|
-
try { const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2299
|
+
try { const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
2142
2300
|
catch (error) { throw mainAgentError(error); }
|
|
2143
2301
|
},
|
|
2144
2302
|
renderCall(args, theme) { return styledTextBlock(workflowControlCall("workflow_resume", args, theme)); },
|
|
@@ -2159,10 +2317,23 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2159
2317
|
if (loaded.run.state === "completed" || loaded.run.state === "failed" || loaded.run.state === "stopped") { terminalRunStates.set(runId, loaded.run.state); continue; }
|
|
2160
2318
|
if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
|
|
2161
2319
|
const previousState = loaded.run.state;
|
|
2162
|
-
await store.updateState((current) =>
|
|
2320
|
+
await store.updateState((current) => {
|
|
2321
|
+
if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state)) return current;
|
|
2322
|
+
const next = { ...current, state: "interrupted" as const };
|
|
2323
|
+
delete next.activeShells;
|
|
2324
|
+
return next;
|
|
2325
|
+
});
|
|
2163
2326
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2164
2327
|
await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
|
|
2165
2328
|
loaded = { ...loaded, run: (await store.load()).run };
|
|
2329
|
+
} else if (loaded.run.activeShells !== undefined) {
|
|
2330
|
+
await store.updateState((current) => {
|
|
2331
|
+
if (["completed", "failed", "stopped"].includes(current.state)) return current;
|
|
2332
|
+
const next = { ...current };
|
|
2333
|
+
delete next.activeShells;
|
|
2334
|
+
return next;
|
|
2335
|
+
});
|
|
2336
|
+
loaded = { ...loaded, run: (await store.load()).run };
|
|
2166
2337
|
}
|
|
2167
2338
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
|
|
2168
2339
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
@@ -2188,7 +2359,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2188
2359
|
if (choice && choice !== "Skip") {
|
|
2189
2360
|
const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
|
|
2190
2361
|
for (const run of toResume) {
|
|
2191
|
-
try { await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx); ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info"); }
|
|
2362
|
+
try { await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false); ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info"); }
|
|
2192
2363
|
catch (err) { ctx.ui.notify(`Cannot resume ${run.metadata.name}: ${err instanceof Error ? err.message : String(err)}`, "warning"); }
|
|
2193
2364
|
}
|
|
2194
2365
|
}
|
|
@@ -2245,10 +2416,24 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2245
2416
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2246
2417
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
2247
2418
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
2248
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function" as const, functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2419
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function" as const, functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2420
|
+
let persistedSnapshot = snapshot;
|
|
2421
|
+
const captureFunctionRole = functionName ? async (role: string, model: ModelSpec): Promise<void> => {
|
|
2422
|
+
const definition = agentDefinitions[role];
|
|
2423
|
+
if (!definition) return;
|
|
2424
|
+
const modelName = `${model.provider}/${model.model}`;
|
|
2425
|
+
const hasProjectRole = projectAgentDefinitions[role] !== undefined;
|
|
2426
|
+
if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName)) return;
|
|
2427
|
+
const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
|
|
2428
|
+
const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
|
|
2429
|
+
const models = [...new Set([...persistedSnapshot.models, modelName])];
|
|
2430
|
+
persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
|
|
2431
|
+
await store.saveSnapshot(persistedSnapshot);
|
|
2432
|
+
} : undefined;
|
|
2249
2433
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2250
2434
|
const initialBudget = budgetRuntime.snapshot();
|
|
2251
|
-
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2435
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
2436
|
+
if (params.foreground) foregroundDeliveries.set(toolCallId, { store, inline: false });
|
|
2252
2437
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2253
2438
|
const background = !params.foreground;
|
|
2254
2439
|
const providerPause = async () => { if (background) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
@@ -2257,7 +2442,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2257
2442
|
runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
|
|
2258
2443
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2259
2444
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2260
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2445
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, captureFunctionRole), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2261
2446
|
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).execution = execution;
|
|
2262
2447
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
2263
2448
|
const finish = execution.result.then(async (value) => {
|
|
@@ -2271,7 +2456,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2271
2456
|
await scheduler.flush();
|
|
2272
2457
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2273
2458
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state)) await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2274
|
-
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot()
|
|
2459
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
|
|
2275
2460
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2276
2461
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
2277
2462
|
const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
@@ -2281,16 +2466,37 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2281
2466
|
});
|
|
2282
2467
|
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
2283
2468
|
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).completion = completion;
|
|
2469
|
+
const deliverFailureContent = (error: unknown): string => {
|
|
2470
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
2471
|
+
return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
|
|
2472
|
+
};
|
|
2473
|
+
const queueForegroundDelivery = async (content: string): Promise<void> => {
|
|
2474
|
+
const delivery = foregroundDeliveries.get(toolCallId);
|
|
2475
|
+
if (!delivery) return;
|
|
2476
|
+
await store.updateState((current) => {
|
|
2477
|
+
if (!current.delivery || current.delivery.state === "delivered") return current;
|
|
2478
|
+
return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
|
|
2479
|
+
});
|
|
2480
|
+
if (delivery.inline) return;
|
|
2481
|
+
scheduleForegroundDelivery(toolCallId, async () => {
|
|
2482
|
+
if (delivery.inline) return;
|
|
2483
|
+
pendingFailureDiagnostics.delete(toolCallId);
|
|
2484
|
+
await deliverTerminal(store, content);
|
|
2485
|
+
});
|
|
2486
|
+
};
|
|
2284
2487
|
if (background) {
|
|
2285
2488
|
void completion.then(async ({ value, resultPath }) => {
|
|
2286
|
-
|
|
2287
|
-
}, (error: unknown) => {
|
|
2288
|
-
|
|
2289
|
-
if (diagnostic) deliverFailure(pi, diagnostic);
|
|
2290
|
-
else deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
2489
|
+
await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2490
|
+
}, async (error: unknown) => {
|
|
2491
|
+
await deliverTerminal(store, deliverFailureContent(error));
|
|
2291
2492
|
});
|
|
2292
2493
|
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2293
2494
|
}
|
|
2495
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2496
|
+
await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2497
|
+
}, async (error: unknown) => {
|
|
2498
|
+
await queueForegroundDelivery(deliverFailureContent(error));
|
|
2499
|
+
});
|
|
2294
2500
|
const { value } = await completion;
|
|
2295
2501
|
const run = (await store.load()).run;
|
|
2296
2502
|
return { content: [{ type: "text" as const, text: JSON.stringify(value) }, { type: "text" as const, text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
@@ -2332,7 +2538,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2332
2538
|
const loadStores = async () => {
|
|
2333
2539
|
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
2334
2540
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
2335
|
-
try { return { store, loaded:
|
|
2541
|
+
try { const loaded = await store.load(); return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } }; }
|
|
2336
2542
|
catch { if (!await store.isComplete()) await store.delete(true).catch(() => undefined); return undefined; }
|
|
2337
2543
|
}));
|
|
2338
2544
|
return entries.filter((entry): entry is { store: RunStore; loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> } } => entry !== undefined);
|
|
@@ -2355,7 +2561,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2355
2561
|
return keepContext ? "dashboard" : "done";
|
|
2356
2562
|
}
|
|
2357
2563
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2358
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2564
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
2359
2565
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2360
2566
|
return keepContext ? "dashboard" : "done";
|
|
2361
2567
|
}
|
|
@@ -2368,10 +2574,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2368
2574
|
if (action === "resume" && run) {
|
|
2369
2575
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2370
2576
|
const patch: unknown = rest.length ? JSON.parse(rest.join(" ")) as unknown : undefined;
|
|
2371
|
-
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2372
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
2577
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
2578
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2373
2579
|
} else {
|
|
2374
|
-
if (run.lifecycle.state === "interrupted") await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2580
|
+
if (run.lifecycle.state === "interrupted") await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
|
|
2375
2581
|
else {
|
|
2376
2582
|
if (run.lifecycle.state === "paused") await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
2377
2583
|
await run.lifecycle.resume();
|
|
@@ -2383,8 +2589,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2383
2589
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
2384
2590
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}" );
|
|
2385
2591
|
if (input === undefined) return keepContext ? "dashboard" : "done";
|
|
2386
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2387
|
-
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "
|
|
2592
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
2593
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
2388
2594
|
return keepContext ? "dashboard" : "done";
|
|
2389
2595
|
}
|
|
2390
2596
|
if (action === "stop" && run) {
|
|
@@ -2528,11 +2734,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2528
2734
|
};
|
|
2529
2735
|
const loadDashboard = async () => {
|
|
2530
2736
|
const loaded = await store.load();
|
|
2737
|
+
const liveRun = withLiveActivities(loaded.run);
|
|
2531
2738
|
const checkpoints = await store.awaitingCheckpoints();
|
|
2532
2739
|
const worktrees = await store.worktrees();
|
|
2533
2740
|
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
2534
2741
|
const agentResults = new Map<string, JsonValue>();
|
|
2535
|
-
for (const agent of
|
|
2742
|
+
for (const agent of liveRun.agents) {
|
|
2536
2743
|
if (agent.state !== "completed" || agent.parentId || !agent.resultPath) continue;
|
|
2537
2744
|
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
2538
2745
|
if (operation) agentResults.set(agent.id, operation.value);
|
|
@@ -2542,15 +2749,15 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2542
2749
|
const reviews = new Map<string, AwaitingCheckpoint>();
|
|
2543
2750
|
const add = (label: string, value: string) => { actions.set(label, `${value} ${store.runId}`); };
|
|
2544
2751
|
const addCopy = (label: string, value: string, artifact: string) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
2545
|
-
if (
|
|
2546
|
-
if (["paused", "interrupted"].includes(
|
|
2547
|
-
if (
|
|
2752
|
+
if (liveRun.state === "running") add("Pause", "pause");
|
|
2753
|
+
if (["paused", "interrupted"].includes(liveRun.state)) add("Resume", "resume");
|
|
2754
|
+
if (liveRun.state === "budget_exhausted") { actions.set("Resume unchanged", `resume ${store.runId}`); actions.set("Adjust budget", `adjust ${store.runId}`); }
|
|
2548
2755
|
for (const decision of await store.pendingWorkflowDecisions()) {
|
|
2549
2756
|
const id = decision.proposalId.slice(0, 8);
|
|
2550
2757
|
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
2551
2758
|
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
2552
2759
|
}
|
|
2553
|
-
if (!terminalStates.has(
|
|
2760
|
+
if (!terminalStates.has(liveRun.state)) add("Stop", "stop");
|
|
2554
2761
|
for (const cp of checkpoints) {
|
|
2555
2762
|
if (ctx.mode === "tui") {
|
|
2556
2763
|
const label = `Review ${cp.name}`;
|
|
@@ -2563,13 +2770,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2563
2770
|
}
|
|
2564
2771
|
if (ctx.mode !== "tui") actions.set("Refresh", "refresh");
|
|
2565
2772
|
else actions.set("Open script in editor", "open-script");
|
|
2566
|
-
if (ctx.mode !== "tui" &&
|
|
2567
|
-
if (terminalStates.has(
|
|
2773
|
+
if (ctx.mode !== "tui" && liveRun.agents.length) actions.set("Agents...", "agents");
|
|
2774
|
+
if (terminalStates.has(liveRun.state)) add("Delete", "delete");
|
|
2568
2775
|
if (ctx.mode === "tui") {
|
|
2569
2776
|
addCopy("Copy run path", store.directory, "run path");
|
|
2570
2777
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2571
2778
|
}
|
|
2572
|
-
return { dashboard: formatWorkflowPhaseDashboard(
|
|
2779
|
+
return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
|
|
2573
2780
|
};
|
|
2574
2781
|
const agentWorktreeFor = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): WorktreeReference | undefined => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
2575
2782
|
const agentActionLabels = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): string[] => {
|
|
@@ -2641,12 +2848,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2641
2848
|
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
2642
2849
|
let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
|
|
2643
2850
|
let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
|
|
2644
|
-
const terminalRows = () => Math.max(1, tuiRows(tui) -
|
|
2851
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
2645
2852
|
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
2646
|
-
const keyLabel = (binding: string, fallback: string) =>
|
|
2647
|
-
const keys = keybindingKeys(keybindings, binding);
|
|
2648
|
-
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
2649
|
-
};
|
|
2853
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
2650
2854
|
const selectedAgentRecord = (): AgentRecord | undefined => {
|
|
2651
2855
|
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
2652
2856
|
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
@@ -2671,7 +2875,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2671
2875
|
ctx.ui.notify(`Cannot open ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
2672
2876
|
} finally {
|
|
2673
2877
|
editorRunning = false;
|
|
2674
|
-
tui.requestRender(true);
|
|
2675
2878
|
}
|
|
2676
2879
|
};
|
|
2677
2880
|
const updateDashboard = async () => {
|
|
@@ -2718,7 +2921,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2718
2921
|
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
2719
2922
|
}, 1000);
|
|
2720
2923
|
timer.unref();
|
|
2721
|
-
return
|
|
2924
|
+
return {
|
|
2722
2925
|
render(width: number) {
|
|
2723
2926
|
renderedWidth = width;
|
|
2724
2927
|
const narrow = width < 80;
|
|
@@ -2749,7 +2952,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2749
2952
|
selectionNeedsScroll = false;
|
|
2750
2953
|
}
|
|
2751
2954
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
2752
|
-
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "
|
|
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] ?? "";
|
|
2753
2956
|
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
2754
2957
|
},
|
|
2755
2958
|
invalidate() {},
|
|
@@ -2759,12 +2962,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2759
2962
|
if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
|
|
2760
2963
|
if (actionMode) {
|
|
2761
2964
|
const options = actionOptions();
|
|
2762
|
-
if (keybindings
|
|
2763
|
-
if (keybindings
|
|
2764
|
-
else if (keybindings
|
|
2765
|
-
else if (keybindings
|
|
2766
|
-
else if (keybindings
|
|
2767
|
-
else if (keybindings
|
|
2965
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) { actionMode = false; dashboardOffset = 0; tui.requestRender(); return; }
|
|
2966
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
2967
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) actionIndex = (actionIndex + 1) % options.length;
|
|
2968
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
2969
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
2970
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2768
2971
|
const action = options[actionIndex];
|
|
2769
2972
|
const agent = selectedAgentRecord();
|
|
2770
2973
|
if (!action || action === "Back") { actionMode = false; dashboardOffset = 0; }
|
|
@@ -2787,30 +2990,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2787
2990
|
return;
|
|
2788
2991
|
}
|
|
2789
2992
|
const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
2790
|
-
if (keybindings
|
|
2791
|
-
if (narrow && detailsMode) { detailsMode = false; selectionNeedsScroll = true; } else done(
|
|
2993
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
2994
|
+
if (narrow && detailsMode) { detailsMode = false; selectionNeedsScroll = true; } else done("Back");
|
|
2792
2995
|
} else if (narrow && detailsMode) {
|
|
2793
|
-
if (keybindings
|
|
2794
|
-
else if (keybindings
|
|
2795
|
-
else if (keybindings
|
|
2996
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
2997
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
2998
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2796
2999
|
if (current?.kind === "agent" && current.agentId) { actionMode = true; actionIndex = 0; }
|
|
2797
3000
|
else if (current?.children.length) { if (expandedNodeIds.has(current.id)) expandedNodeIds.delete(current.id); else expandedNodeIds.add(current.id); }
|
|
2798
3001
|
}
|
|
2799
|
-
} else if (keybindings
|
|
3002
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
2800
3003
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
2801
3004
|
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
2802
|
-
} else if (keybindings
|
|
3005
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
2803
3006
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
2804
3007
|
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
2805
|
-
} else if (keybindings
|
|
3008
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
2806
3009
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
2807
3010
|
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
2808
|
-
} else if (keybindings
|
|
3011
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
2809
3012
|
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
2810
3013
|
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
2811
|
-
} else if (keybindings
|
|
2812
|
-
else if (keybindings
|
|
2813
|
-
else if (keybindings
|
|
3014
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
3015
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
3016
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
2814
3017
|
if (narrow) detailsMode = true;
|
|
2815
3018
|
else if (current?.kind === "agent" && current.agentId) { actionMode = true; actionIndex = 0; }
|
|
2816
3019
|
else if (current?.children.length) { if (expandedNodeIds.has(current.id)) expandedNodeIds.delete(current.id); else expandedNodeIds.add(current.id); }
|
|
@@ -2818,10 +3021,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2818
3021
|
tui.requestRender();
|
|
2819
3022
|
},
|
|
2820
3023
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "
|
|
2824
|
-
if (!actionChoice || actionChoice === "
|
|
3024
|
+
};
|
|
3025
|
+
})
|
|
3026
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
3027
|
+
if (!actionChoice || actionChoice === "Back") { stores = await loadStores(); break; }
|
|
2825
3028
|
if (actionChoice === "Agents...") { await selectAgent(view); continue; }
|
|
2826
3029
|
if (actionChoice.startsWith("__workflow_agent__:")) { await selectAgent(view, actionChoice.slice("__workflow_agent__:".length)); continue; }
|
|
2827
3030
|
if (actionChoice.startsWith("__workflow_fork__:")) {
|
|
@@ -2861,7 +3064,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2861
3064
|
const currentLayout = layout();
|
|
2862
3065
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
2863
3066
|
offset = Math.min(offset, maxOffset);
|
|
2864
|
-
const
|
|
3067
|
+
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
3068
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
3069
|
+
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
2865
3070
|
const controls = currentLayout.compactControls
|
|
2866
3071
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
2867
3072
|
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
@@ -2875,12 +3080,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2875
3080
|
},
|
|
2876
3081
|
invalidate() {},
|
|
2877
3082
|
handleInput(data: string) {
|
|
2878
|
-
if (keybindings
|
|
2879
|
-
else if (keybindings
|
|
2880
|
-
else if (keybindings
|
|
2881
|
-
else if (keybindings
|
|
2882
|
-
else if (keybindings
|
|
2883
|
-
else if (keybindings
|
|
3083
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
3084
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) selectedIndex = (selectedIndex + 1) % options.length;
|
|
3085
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) move(-layout().contentViewport);
|
|
3086
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) move(layout().contentViewport);
|
|
3087
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex] as "Approve" | "Reject");
|
|
3088
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) done(undefined);
|
|
2884
3089
|
tui.requestRender();
|
|
2885
3090
|
},
|
|
2886
3091
|
}, theme);
|