pi-extensible-workflows 3.0.0 → 3.1.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 +15 -10
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/host.d.ts +40 -1
- package/dist/src/host.js +517 -129
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +134 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +123 -31
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +50 -16
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/host.ts +455 -122
- package/src/index.ts +4 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +52 -7
- package/src/utils.ts +39 -5
- package/src/validation.ts +103 -31
package/src/host.ts
CHANGED
|
@@ -6,16 +6,16 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { Type } from "@earendil-works/pi-ai";
|
|
7
7
|
import { Value } from "typebox/value";
|
|
8
8
|
import { copyToClipboard, getAgentDir, highlightCode, truncateToVisualLines, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery, type
|
|
9
|
+
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery, type SessionFactory } from "./agent-execution.js";
|
|
10
10
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
11
11
|
import { acquireSessionLease, listRunIds, RunStore, SessionLease, structuralPath as operationPath } from "./persistence.js";
|
|
12
12
|
import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
13
13
|
import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
14
|
-
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
15
|
-
import { launchScriptForSnapshot, loadAgentDefinitions,
|
|
14
|
+
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelAliasErrorName, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
15
|
+
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
16
16
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
17
17
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
18
|
-
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 AgentAttemptSummary, type AgentOptions, type AgentRecord, type BudgetApprovalRequest, type BudgetEvent, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowErrorCode, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowFunctionContext, type WorkflowExecution, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowRunContext, type WorkflowSiblingAgent, type WorkflowWorktreeReference } from "./types.js";
|
|
18
|
+
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 AgentAttemptSummary, type AgentOptions, type AgentRecord, type AgentResourcePolicy, type BudgetApprovalRequest, type BudgetEvent, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowBridge, type WorkflowCatalogFunction, type WorkflowCatalogIndex, type WorkflowCatalogVariable, type WorkflowCheckpointState, type WorkflowErrorCode, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowFunctionContext, type WorkflowExecution, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowRetryProvenance, type WorkflowRunContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowSiblingAgent, type WorkflowWorktreeReference } from "./types.js";
|
|
19
19
|
const SETTLED_AGENT_STATES: ReadonlySet<import("./types.js").AgentState> = new Set(["completed", "failed", "cancelled"]);
|
|
20
20
|
export interface WorkflowProgressStyles {
|
|
21
21
|
accent(text: string): string;
|
|
@@ -26,7 +26,25 @@ export interface WorkflowProgressStyles {
|
|
|
26
26
|
dim(text: string): string;
|
|
27
27
|
bold(text: string): string;
|
|
28
28
|
}
|
|
29
|
+
function snapshotResourcePolicy(snapshot: Readonly<LaunchSnapshot>, cwd: string, projectTrusted: boolean, globalSettingsPath: string): AgentResourcePolicy {
|
|
30
|
+
const empty = { skills: [], extensions: [] };
|
|
31
|
+
return { globalSettingsPath, projectSettingsPath: join(cwd, ".pi", "pi-extensible-workflows", "settings.json"), projectTrusted, global: empty, project: empty, effective: snapshot.settings.disabledAgentResources ?? empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
32
|
+
}
|
|
29
33
|
const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
34
|
+
type WorkflowLaunchSettings = { settings: Readonly<WorkflowSettings>; resolution: WorkflowSettingsResolution; resourcePolicy: AgentResourcePolicy; modelSettingsPath: string };
|
|
35
|
+
function workflowLaunchSettings(cwd: string, projectTrusted: boolean, globalSettingsPath: string, concurrency?: number): WorkflowLaunchSettings {
|
|
36
|
+
const resolution = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
37
|
+
const settings = Object.freeze({ ...resolution.effective, ...(concurrency === undefined ? {} : { concurrency }) });
|
|
38
|
+
return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath), modelSettingsPath: resolution.sources.modelAliases };
|
|
39
|
+
}
|
|
40
|
+
function frozenResourcePolicy(policy: AgentResourcePolicy): () => AgentResourcePolicy { return () => structuredClone(policy); }
|
|
41
|
+
function resumedSnapshotSettings(snapshot: Readonly<LaunchSnapshot>, resolution: WorkflowSettingsResolution, modelAliases: Readonly<Record<string, string>>): { settings: WorkflowSettings; settingsSources?: NonNullable<LaunchSnapshot["settingsSources"]> } {
|
|
42
|
+
const settings: WorkflowSettings = { ...snapshot.settings, concurrency: snapshot.settingsSources === undefined || snapshot.settingsSources.concurrency === "per-run options" ? snapshot.settings.concurrency : resolution.effective.concurrency, modelAliases };
|
|
43
|
+
if (resolution.effective.disabledAgentResources === undefined) delete settings.disabledAgentResources;
|
|
44
|
+
else settings.disabledAgentResources = resolution.effective.disabledAgentResources;
|
|
45
|
+
const settingsSources = snapshot.settingsSources === undefined ? undefined : { ...snapshot.settingsSources, modelAliases: resolution.sources.modelAliases, disabledAgentResources: resolution.sources.disabledAgentResources, concurrency: snapshot.settingsSources.concurrency === "per-run options" ? "per-run options" : resolution.sources.concurrency };
|
|
46
|
+
return { settings, ...(settingsSources === undefined ? {} : { settingsSources }) };
|
|
47
|
+
}
|
|
30
48
|
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
31
49
|
|
|
32
50
|
function workflowDetail(message: string): string {
|
|
@@ -162,7 +180,91 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
162
180
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
163
181
|
|
|
164
182
|
type WorkflowToolUpdate = { content: [{ type: "text"; text: string }]; details: { runId: string; run: PersistedRun } };
|
|
165
|
-
|
|
183
|
+
export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
|
|
184
|
+
export interface WorkflowPhaseAgentCounts { total: number; completed: number; running: number; failed: number; cancelled: number; pending: number }
|
|
185
|
+
export interface WorkflowPhaseView { id: string; name: string; occurrence: number; state: WorkflowPhaseState; status: WorkflowPhaseState; observed: boolean; afterAgent?: number; agents: readonly AgentRecord[]; counts: WorkflowPhaseAgentCounts }
|
|
186
|
+
export interface WorkflowPhaseModel { declaredPhases: readonly string[]; phases: readonly WorkflowPhaseView[]; currentPhaseIndex?: number; currentPhaseId?: string; counts: Readonly<Partial<Record<WorkflowPhaseState, number>>>; unassignedAgents?: readonly AgentRecord[] }
|
|
187
|
+
type WorkflowPhaseSource = readonly string[] | Pick<LaunchSnapshot, "phases"> | undefined;
|
|
188
|
+
function phaseNames(source: WorkflowPhaseSource): string[] {
|
|
189
|
+
const phases: readonly unknown[] = source === undefined ? [] : Array.isArray(source) ? source : (source as Pick<LaunchSnapshot, "phases">).phases ?? [];
|
|
190
|
+
return phases.filter((phase): phase is string => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
191
|
+
}
|
|
192
|
+
function phaseAgentCounts(agents: readonly AgentRecord[]): WorkflowPhaseAgentCounts {
|
|
193
|
+
const counts: WorkflowPhaseAgentCounts = { total: agents.length, completed: 0, running: 0, failed: 0, cancelled: 0, pending: 0 };
|
|
194
|
+
for (const agent of agents) {
|
|
195
|
+
if (agent.state === "completed") counts.completed += 1;
|
|
196
|
+
else if (agent.state === "running") counts.running += 1;
|
|
197
|
+
else if (agent.state === "failed") counts.failed += 1;
|
|
198
|
+
else if (agent.state === "cancelled") counts.cancelled += 1;
|
|
199
|
+
else counts.pending += 1;
|
|
200
|
+
}
|
|
201
|
+
return counts;
|
|
202
|
+
}
|
|
203
|
+
function phaseState(runState: RunState, counts: WorkflowPhaseAgentCounts, isLatest: boolean): WorkflowPhaseState {
|
|
204
|
+
if (!isLatest) return "completed";
|
|
205
|
+
if (runState === "failed") return "failed";
|
|
206
|
+
if (runState === "stopped") return "cancelled";
|
|
207
|
+
if (runState === "interrupted") return "interrupted";
|
|
208
|
+
if (runState === "budget_exhausted") return "budget_exhausted";
|
|
209
|
+
if (counts.failed > 0) return "failed";
|
|
210
|
+
if (counts.cancelled > 0) return "cancelled";
|
|
211
|
+
if (counts.running > 0 || counts.pending > 0) return "running";
|
|
212
|
+
return runState === "completed" ? "completed" : "running";
|
|
213
|
+
}
|
|
214
|
+
export function buildWorkflowPhaseModel(run: Pick<PersistedRun, "state" | "phase" | "phaseHistory" | "agents">, source?: WorkflowPhaseSource): WorkflowPhaseModel {
|
|
215
|
+
const declaredPhases = phaseNames(source);
|
|
216
|
+
const rawHistory: readonly unknown[] = Array.isArray(run.phaseHistory) ? run.phaseHistory : [];
|
|
217
|
+
const observed: Array<{ name: string; afterAgent: number }> = [];
|
|
218
|
+
let boundary = 0;
|
|
219
|
+
for (const record of rawHistory) {
|
|
220
|
+
if (!object(record) || typeof record.phase !== "string" || !record.phase.trim() || typeof record.afterAgent !== "number" || !Number.isSafeInteger(record.afterAgent)) continue;
|
|
221
|
+
boundary = Math.max(boundary, Math.min(run.agents.length, Math.max(0, record.afterAgent)));
|
|
222
|
+
observed.push({ name: record.phase.trim(), afterAgent: boundary });
|
|
223
|
+
}
|
|
224
|
+
if (!observed.length && typeof run.phase === "string" && run.phase.trim()) observed.push({ name: run.phase.trim(), afterAgent: 0 });
|
|
225
|
+
const observedEntries = observed.map((entry, index) => ({ ...entry, index, agents: run.agents.slice(entry.afterAgent, observed[index + 1]?.afterAgent ?? run.agents.length) }));
|
|
226
|
+
const matchedDeclarations = new Set<number>();
|
|
227
|
+
const declarationIndices = observedEntries.map((entry) => {
|
|
228
|
+
const index = declaredPhases.findIndex((name, candidate) => !matchedDeclarations.has(candidate) && name === entry.name);
|
|
229
|
+
if (index >= 0) matchedDeclarations.add(index);
|
|
230
|
+
return index >= 0 ? index : undefined;
|
|
231
|
+
});
|
|
232
|
+
const entries: Array<{ name: string; observedIndex?: number; declarationIndex?: number }> = observedEntries.map((entry, index) => ({ name: entry.name, observedIndex: index, ...(declarationIndices[index] === undefined ? {} : { declarationIndex: declarationIndices[index] }) }));
|
|
233
|
+
for (const [declarationIndex, name] of declaredPhases.entries()) {
|
|
234
|
+
if (matchedDeclarations.has(declarationIndex)) continue;
|
|
235
|
+
const insertion = entries.findIndex((entry) => entry.declarationIndex !== undefined && entry.declarationIndex > declarationIndex);
|
|
236
|
+
const pending = { name };
|
|
237
|
+
if (insertion < 0) entries.push(pending); else entries.splice(insertion, 0, pending);
|
|
238
|
+
}
|
|
239
|
+
const occurrences = new Map<string, number>();
|
|
240
|
+
const phases = entries.map((entry) => {
|
|
241
|
+
const occurrence = (occurrences.get(entry.name) ?? 0) + 1;
|
|
242
|
+
occurrences.set(entry.name, occurrence);
|
|
243
|
+
const observation = entry.observedIndex === undefined ? undefined : observedEntries[entry.observedIndex];
|
|
244
|
+
const agents = observation?.agents ?? [];
|
|
245
|
+
const counts = phaseAgentCounts(agents);
|
|
246
|
+
const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1) : "not started";
|
|
247
|
+
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, status: state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
|
|
248
|
+
});
|
|
249
|
+
let currentPhaseIndex: number | undefined;
|
|
250
|
+
for (let index = phases.length - 1; index >= 0; index -= 1) { if (phases[index]?.observed) { currentPhaseIndex = index; break; } }
|
|
251
|
+
const counts: Partial<Record<WorkflowPhaseState, number>> = {};
|
|
252
|
+
for (const phase of phases) counts[phase.state] = (counts[phase.state] ?? 0) + 1;
|
|
253
|
+
const current = currentPhaseIndex === undefined ? undefined : phases[currentPhaseIndex];
|
|
254
|
+
const assigned = new Set(observedEntries.flatMap(({ agents }) => agents.map((agent) => agent.id)));
|
|
255
|
+
const unassignedAgents = run.agents.filter((agent) => !assigned.has(agent.id));
|
|
256
|
+
const result: WorkflowPhaseModel = { declaredPhases, phases, counts };
|
|
257
|
+
if (current !== undefined && currentPhaseIndex !== undefined) { result.currentPhaseIndex = currentPhaseIndex; result.currentPhaseId = current.id; }
|
|
258
|
+
if (unassignedAgents.length) result.unassignedAgents = unassignedAgents;
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
export interface WorkflowPhaseSelection { phaseId?: string | undefined; agentId?: string | undefined }
|
|
262
|
+
export function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection {
|
|
263
|
+
const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
|
|
264
|
+
if (!phase) return {};
|
|
265
|
+
const agentId = phase.agents.some((agent) => agent.id === selection.agentId) ? selection.agentId : phase.agents[0]?.id;
|
|
266
|
+
return { phaseId: phase.id, ...(agentId === undefined ? {} : { agentId }) };
|
|
267
|
+
}
|
|
166
268
|
type AgentGroup = { label: string; entries: readonly { agent: AgentRecord; index: number; depth: number }[] };
|
|
167
269
|
function agentGroupKey(agent: AgentRecord): string { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
168
270
|
function agentGroupLabel(agents: readonly AgentRecord[]): string {
|
|
@@ -175,7 +277,8 @@ function agentGroups(agents: readonly AgentRecord[], allAgents: readonly AgentRe
|
|
|
175
277
|
const groups = new Map<string, { agents: Array<{ agent: AgentRecord; index: number; depth: number }> }>();
|
|
176
278
|
for (const [index, agent] of agents.entries()) {
|
|
177
279
|
let depth = 0;
|
|
178
|
-
|
|
280
|
+
const seen = new Set<string>([agent.id]);
|
|
281
|
+
for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) { if (seen.has(parent)) break; seen.add(parent); depth += 1; }
|
|
179
282
|
const key = agentGroupKey(agent);
|
|
180
283
|
const group = groups.get(key) ?? { agents: [] };
|
|
181
284
|
group.agents.push({ agent, index, depth });
|
|
@@ -242,6 +345,99 @@ function textBlock(text: string) {
|
|
|
242
345
|
invalidate() {},
|
|
243
346
|
};
|
|
244
347
|
}
|
|
348
|
+
function styledTextBlock(text: string) {
|
|
349
|
+
return {
|
|
350
|
+
render(width: number) {
|
|
351
|
+
return truncateWorkflowProgress(text, width);
|
|
352
|
+
},
|
|
353
|
+
invalidate() {},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function workflowCatalogBlock(text: string, expanded: boolean) {
|
|
357
|
+
return {
|
|
358
|
+
render(width: number) {
|
|
359
|
+
const safeWidth = Math.max(1, width);
|
|
360
|
+
if (!expanded) return truncateWorkflowProgress(text, safeWidth);
|
|
361
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines.map((line) => line.trimEnd());
|
|
362
|
+
},
|
|
363
|
+
invalidate() {},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function catalogText(value: string): string { return value.replace(/\s+/g, " ").trim(); }
|
|
368
|
+
|
|
369
|
+
type CatalogToolResult = { details?: unknown; content?: readonly { type: string; text?: string }[] };
|
|
370
|
+
|
|
371
|
+
function catalogResultValue(result: CatalogToolResult): unknown {
|
|
372
|
+
if (result.details !== undefined) return result.details;
|
|
373
|
+
const text = result.content?.find((entry) => entry.type === "text")?.text;
|
|
374
|
+
if (!text) return undefined;
|
|
375
|
+
try { return JSON.parse(text) as unknown; } catch { return text; }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isCatalogIndex(value: unknown): value is WorkflowCatalogIndex {
|
|
379
|
+
return object(value) && Array.isArray(value.functions) && Array.isArray(value.variables);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isCatalogFunction(value: unknown): value is WorkflowCatalogFunction {
|
|
383
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.input) && object(value.output);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isCatalogVariable(value: unknown): value is WorkflowCatalogVariable {
|
|
387
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.schema);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isCatalogError(value: unknown): value is { error: { message: string } } {
|
|
391
|
+
return object(value) && object(value.error) && typeof value.error.message === "string";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function catalogSectionTitle(label: string, count: number, theme: Theme): string {
|
|
395
|
+
return theme.fg("accent", theme.bold(`${label} (${String(count)})`));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function catalogIndexEntries(entries: readonly { name: string; description: string }[], theme: Theme): string[] {
|
|
399
|
+
const width = Math.max(0, ...entries.map((entry) => entry.name.length));
|
|
400
|
+
return entries.map((entry) => ` ${theme.fg("accent", entry.name.padEnd(width))} ${theme.fg("toolOutput", catalogText(entry.description))}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function formatCatalogIndex(catalog: WorkflowCatalogIndex, theme: Theme): string {
|
|
404
|
+
const aliases = Object.prototype.propertyIsEnumerable.call(catalog, "modelAliases") ? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static" as const, provenance: "settings" })) : catalog.modelAliasEntries ?? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static" as const, provenance: "settings" }));
|
|
405
|
+
const aliasWidth = Math.max(0, ...aliases.map(({ name }) => name.length));
|
|
406
|
+
const aliasLines = aliases.map(({ name, kind, provenance }) => ` ${theme.fg("accent", name.padEnd(aliasWidth))} ${theme.fg("toolOutput", `${kind} · ${provenance}`)}`);
|
|
407
|
+
return [
|
|
408
|
+
catalogSectionTitle("Functions", catalog.functions.length, theme),
|
|
409
|
+
...catalogIndexEntries(catalog.functions, theme),
|
|
410
|
+
"",
|
|
411
|
+
catalogSectionTitle("Variables", catalog.variables.length, theme),
|
|
412
|
+
...catalogIndexEntries(catalog.variables, theme),
|
|
413
|
+
"",
|
|
414
|
+
catalogSectionTitle("Model aliases", aliases.length, theme),
|
|
415
|
+
...aliasLines,
|
|
416
|
+
].join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function catalogSchemaLines(schema: unknown, theme: Theme): string[] {
|
|
420
|
+
const json = JSON.stringify(schema, null, 2);
|
|
421
|
+
return json.split("\n").map((line) => ` ${theme.fg("toolOutput", line)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function formatCatalogDetail(value: WorkflowCatalogFunction | WorkflowCatalogVariable | import("./types.js").WorkflowCatalogModelAlias, expanded: boolean, theme: Theme): string {
|
|
425
|
+
if ("kind" in value) return [theme.fg("accent", theme.bold("Model alias")), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", `${value.kind} · ${value.provenance}`)}`].join("\n");
|
|
426
|
+
const kind = "input" in value ? "Function" : "Variable";
|
|
427
|
+
if (!expanded) return [theme.fg("accent", theme.bold(kind)), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", catalogText(value.description))}`, ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)} ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", catalogText(value.headline))}`].join("\n");
|
|
428
|
+
const lines = [theme.fg("accent", theme.bold(`${kind}: ${value.name}`)), `${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.description)}`, "", theme.fg("accent", theme.bold("Extension")), ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)}`, ` ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", value.headline)}`, ` ${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.extensionDescription)}`, "", theme.fg("accent", theme.bold("Schema"))];
|
|
429
|
+
if ("input" in value) lines.push(theme.fg("muted", "Input schema"), ...catalogSchemaLines(value.input, theme), "", theme.fg("muted", "Output schema"), ...catalogSchemaLines(value.output, theme));
|
|
430
|
+
else lines.push(theme.fg("muted", "Variable schema"), ...catalogSchemaLines(value.schema, theme));
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatWorkflowCatalog(value: unknown, expanded: boolean, theme: Theme): string {
|
|
435
|
+
if (isCatalogIndex(value)) return formatCatalogIndex(value, theme);
|
|
436
|
+
if (isCatalogFunction(value) || isCatalogVariable(value)) return formatCatalogDetail(value, expanded, theme);
|
|
437
|
+
if (object(value) && typeof value.name === "string" && (value.kind === "static" || value.kind === "dynamic")) return formatCatalogDetail(value as unknown as import("./types.js").WorkflowCatalogModelAlias, expanded, theme);
|
|
438
|
+
if (isCatalogError(value)) return theme.fg("error", value.error.message);
|
|
439
|
+
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
440
|
+
}
|
|
245
441
|
|
|
246
442
|
const ANSI_SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`);
|
|
247
443
|
export function truncateWorkflowProgress(text: string, width: number): string[] {
|
|
@@ -264,7 +460,7 @@ function themeWorkflowProgressStyles(theme: Theme): WorkflowProgressStyles {
|
|
|
264
460
|
warning: (text) => theme.fg("warning", text),
|
|
265
461
|
muted: (text) => theme.fg("muted", text),
|
|
266
462
|
dim: (text) => theme.fg("dim", text),
|
|
267
|
-
bold: (text) => theme.bold(text),
|
|
463
|
+
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
268
464
|
};
|
|
269
465
|
}
|
|
270
466
|
function workflowProgressBlock(run: PersistedRun, theme: Theme) {
|
|
@@ -318,28 +514,29 @@ function navigatorRunLabels(entries: readonly { store: RunStore; loaded: { run:
|
|
|
318
514
|
});
|
|
319
515
|
}
|
|
320
516
|
|
|
321
|
-
function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord
|
|
322
|
-
const
|
|
323
|
-
const parts: string[] = agent.
|
|
517
|
+
export function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath = false): string[] {
|
|
518
|
+
const leaf = agent.label ?? agent.name;
|
|
519
|
+
const parts: string[] = includeStructuralPath && agent.structuralPath?.length ? [agent.structuralPath.join(" > ")] : [];
|
|
520
|
+
if (agent.parentBreadcrumb) parts.push(agent.parentBreadcrumb);
|
|
521
|
+
const ancestors: string[] = [];
|
|
324
522
|
const seen = new Set<string>([agent.id]);
|
|
325
523
|
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
326
|
-
if (seen.has(parentId)) break;
|
|
524
|
+
if (seen.has(parentId)) break;
|
|
327
525
|
seen.add(parentId);
|
|
328
526
|
const parent = byId.get(parentId);
|
|
329
|
-
if (parent)
|
|
330
|
-
|
|
527
|
+
if (!parent) break;
|
|
528
|
+
ancestors.push(parent.label ?? parent.name);
|
|
331
529
|
}
|
|
332
|
-
parts.push(
|
|
530
|
+
parts.push(...ancestors.reverse(), leaf);
|
|
333
531
|
return parts;
|
|
334
532
|
}
|
|
335
|
-
function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord
|
|
336
|
-
|
|
337
|
-
return parts.length > 1 ? parts.join(" > ") : parts[0] ?? "";
|
|
533
|
+
export function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath = false): string {
|
|
534
|
+
return agentBreadcrumbParts(agent, byId, includeStructuralPath).join(" > ");
|
|
338
535
|
}
|
|
339
536
|
function styledAgentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, styles: WorkflowProgressStyles): string {
|
|
340
537
|
const parts = agentBreadcrumbParts(agent, byId);
|
|
341
538
|
if (parts.length <= 1) return parts[0] ?? "";
|
|
342
|
-
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${parts[parts.length - 1] ?? ""}`;
|
|
539
|
+
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
343
540
|
}
|
|
344
541
|
|
|
345
542
|
function formatAgentActivity(agent: AgentRecord, spinner: string, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
@@ -395,11 +592,13 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
395
592
|
`Launch cwd: ${run.cwd}`,
|
|
396
593
|
...formatCompactBudgetStatus(run),
|
|
397
594
|
`Launch models: ${snapshot.models.join(", ") || "(none)"}`,
|
|
595
|
+
`Settings: concurrency=${String(snapshot.settings.concurrency)}`,
|
|
398
596
|
];
|
|
399
597
|
if (run.error) lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
400
598
|
if (run.events?.length) lines.push(...run.events.map((event) => `Warning: ${event.message}`));
|
|
401
599
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
402
600
|
if (aliases && Object.keys(aliases).length) lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
|
|
601
|
+
if (snapshot.settingsSources) lines.push(`Settings sources: concurrency=${snapshot.settingsSources.concurrency}, modelAliases=${snapshot.settingsSources.modelAliases}, disabledAgentResources=${snapshot.settingsSources.disabledAgentResources}`);
|
|
403
602
|
lines.push("Agents / ownership:");
|
|
404
603
|
if (!run.agents.length) lines.push(" (none)");
|
|
405
604
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
@@ -421,6 +620,60 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
421
620
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
422
621
|
return lines.join("\n");
|
|
423
622
|
}
|
|
623
|
+
export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection: WorkflowPhaseSelection = {}, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string[] {
|
|
624
|
+
const safeWidth = Math.max(1, width);
|
|
625
|
+
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
626
|
+
const wrap = (text: string, limit = safeWidth): string[] => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
|
|
627
|
+
const phaseStyle = (state: WorkflowPhaseState): ((text: string) => string) => state === "completed" ? (text) => styles.success(text) : state === "failed" || state === "cancelled" ? (text) => styles.error(text) : state === "running" ? (text) => styles.accent(text) : state === "interrupted" || state === "budget_exhausted" ? (text) => styles.warning(text) : (text) => styles.muted(text);
|
|
628
|
+
const phaseName = (phase: WorkflowPhaseView): string => `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`;
|
|
629
|
+
const phaseCounts = (phase: WorkflowPhaseView): string => `agents completed=${String(phase.counts.completed)} running=${String(phase.counts.running)} failed=${String(phase.counts.failed)} cancelled=${String(phase.counts.cancelled)} pending=${String(phase.counts.pending)}`;
|
|
630
|
+
const phaseLine = (phase: WorkflowPhaseView, selected: boolean): string => `${selected ? "→" : " "} ${phaseName(phase)} · ${phaseStyle(phase.state)(phase.state)} · ${phaseCounts(phase)}`;
|
|
631
|
+
const stateNames: readonly WorkflowPhaseState[] = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
632
|
+
const statusSummary = stateNames.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
|
|
633
|
+
const selectedIndex = selection.phaseId ? model.phases.findIndex((phase) => phase.id === selection.phaseId) : -1;
|
|
634
|
+
const activeIndex = selectedIndex >= 0 ? selectedIndex : model.currentPhaseIndex ?? (model.phases.length ? 0 : -1);
|
|
635
|
+
const selectedPhase = activeIndex >= 0 ? model.phases[activeIndex] : undefined;
|
|
636
|
+
const lines: string[] = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
637
|
+
if (run.error) lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
638
|
+
lines.push(`phase: ${run.phase ?? selectedPhase?.name ?? "none"}`, `Run state: ${run.state}`, `Phases: ${statusSummary}`);
|
|
639
|
+
for (const event of run.events ?? []) lines.push(styles.warning(`Warning: ${event.message}`));
|
|
640
|
+
lines.push(...formatCompactBudgetStatus(run));
|
|
641
|
+
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
642
|
+
const renderAgentLines = (agents: readonly AgentRecord[], selectedAgentId: string | undefined): string[] => {
|
|
643
|
+
if (!agents.length) return [styles.muted("Agents: none")];
|
|
644
|
+
const rendered: string[] = [];
|
|
645
|
+
for (const agent of agents) {
|
|
646
|
+
const selected = agent.id === selectedAgentId;
|
|
647
|
+
const stateStyle = progressStyleForState(agent.state, styles);
|
|
648
|
+
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
649
|
+
rendered.push(`${selected ? "→" : " "} ${stateStyle(icon)} ${styledAgentBreadcrumb(agent, byId, styles)} · ${stateStyle(agent.state)}`);
|
|
650
|
+
if (agent.state === "failed") {
|
|
651
|
+
const error = agent.attemptDetails?.at(-1)?.error;
|
|
652
|
+
if (error) rendered.push(styles.error(` error: ${error.code}: ${error.message}`));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return rendered;
|
|
656
|
+
};
|
|
657
|
+
if (!model.phases.length) {
|
|
658
|
+
lines.push(styles.muted("Phases: no declared or observed phases (legacy snapshot)"), styles.bold("Agents"), ...renderAgentLines(run.agents, selection.agentId));
|
|
659
|
+
return lines.flatMap((line) => wrap(line));
|
|
660
|
+
}
|
|
661
|
+
const selectedAgent = selectedPhase?.agents.find((agent) => agent.id === selection.agentId) ?? selectedPhase?.agents[0];
|
|
662
|
+
if (safeWidth >= 80) {
|
|
663
|
+
const sidebarWidth = Math.min(38, Math.max(24, Math.floor(safeWidth * 0.36)));
|
|
664
|
+
const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
|
|
665
|
+
const sidebar = [styles.bold("Phases"), ...model.phases.flatMap((phase, index) => wrap(phaseLine(phase, index === activeIndex), sidebarWidth))];
|
|
666
|
+
const detail = selectedPhase ? [styles.bold(`Phase: ${phaseName(selectedPhase)}`), ...wrap(`Status: ${phaseStyle(selectedPhase.state)(selectedPhase.state)}`, detailWidth), ...wrap(phaseCounts(selectedPhase), detailWidth), "Agents", ...renderAgentLines(selectedPhase.agents, selectedAgent?.id).flatMap((line) => wrap(line, detailWidth))] : [styles.muted("No phase is selected")];
|
|
667
|
+
const rows = Math.max(sidebar.length, detail.length);
|
|
668
|
+
for (let index = 0; index < rows; index += 1) lines.push(`${sidebar[index] ?? ""} | ${detail[index] ?? ""}`);
|
|
669
|
+
} else {
|
|
670
|
+
lines.push(styles.bold("Phases"));
|
|
671
|
+
for (const [index, phase] of model.phases.entries()) lines.push(...wrap(phaseLine(phase, index === activeIndex)));
|
|
672
|
+
if (selectedPhase) lines.push("", styles.bold(`Selected phase: ${phaseName(selectedPhase)}`), ...wrap(`Status: ${phaseStyle(selectedPhase.state)(selectedPhase.state)}`), ...wrap(phaseCounts(selectedPhase)), styles.bold("Agents"), ...renderAgentLines(selectedPhase.agents, selectedAgent?.id).flatMap((line) => wrap(line)));
|
|
673
|
+
}
|
|
674
|
+
if (model.unassignedAgents?.length) lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
675
|
+
return lines.flatMap((line) => wrap(line));
|
|
676
|
+
}
|
|
424
677
|
function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string {
|
|
425
678
|
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
426
679
|
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
@@ -862,6 +1115,33 @@ function contextHostCapabilities(ctx: unknown): ContextHostCapabilities {
|
|
|
862
1115
|
const getAvailable = registry.getAvailable;
|
|
863
1116
|
return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
864
1117
|
}
|
|
1118
|
+
function modelInventory(root: ModelSpec | undefined, registry: ModelRegistryCapability | undefined): { knownModels: ReadonlySet<string>; availableModels: ReadonlySet<string> } {
|
|
1119
|
+
const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
1120
|
+
const available = registry?.getAvailable?.() ?? registry?.getAll?.() ?? [];
|
|
1121
|
+
const knownModels = new Set(all.map((model) => `${model.provider}/${model.id}`));
|
|
1122
|
+
const availableModels = new Set(available.map((model) => `${model.provider}/${model.id}`));
|
|
1123
|
+
const rootName = root?.provider && root.model ? `${root.provider}/${root.model}` : undefined;
|
|
1124
|
+
if (rootName) { knownModels.add(rootName); availableModels.add(rootName); }
|
|
1125
|
+
return { knownModels, availableModels };
|
|
1126
|
+
}
|
|
1127
|
+
function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined } {
|
|
1128
|
+
const model = object(ctx) && object(ctx.model) && typeof ctx.model.provider === "string" && typeof ctx.model.id === "string" ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
1129
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry };
|
|
1130
|
+
}
|
|
1131
|
+
async function resolveLaunchAliases(registry: WorkflowRegistryApi, staticAliases: Readonly<Record<string, string>>, context: Readonly<WorkflowModelAliasResolverContext>, availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath: string): Promise<{ aliases: Readonly<Record<string, string>>; dynamicNames: readonly string[] }> {
|
|
1132
|
+
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
1133
|
+
const dynamicNames = Object.keys(dynamic);
|
|
1134
|
+
try {
|
|
1135
|
+
const aliases = validateModelAliases({ ...dynamic, ...staticAliases }, settingsPath);
|
|
1136
|
+
validateModelAliasAvailability(aliases, dynamicNames, availableModels, knownModels, settingsPath);
|
|
1137
|
+
return { aliases, dynamicNames };
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
const name = modelAliasErrorName(error);
|
|
1140
|
+
const descriptor = name && typeof registry.modelAliases === "function" ? registry.modelAliases().find((candidate) => candidate.name === name) : undefined;
|
|
1141
|
+
if (descriptor && errorCode(error) !== "CANCELLED") throw new WorkflowError(errorCode(error) ?? "CONFIG_ERROR", `${errorText(error)} (extension: ${descriptor.headline})`);
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
865
1145
|
type UiSelect = (title: string, options: string[]) => Promise<string | undefined>;
|
|
866
1146
|
type UiInput = (title: string, placeholder?: string) => Promise<string | undefined>;
|
|
867
1147
|
type UiSetStatus = (key: string, text?: string) => void;
|
|
@@ -993,10 +1273,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
993
1273
|
};
|
|
994
1274
|
const phaseBridge = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => {
|
|
995
1275
|
let cursor = 0;
|
|
996
|
-
let executionPhase: string | undefined;
|
|
997
1276
|
return async (phase: string): Promise<void> => {
|
|
998
|
-
if (phase === executionPhase) return;
|
|
999
|
-
executionPhase = phase;
|
|
1000
1277
|
await scheduler.flush();
|
|
1001
1278
|
await lifecycle.enter();
|
|
1002
1279
|
try {
|
|
@@ -1167,13 +1444,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1167
1444
|
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) });
|
|
1168
1445
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1169
1446
|
};
|
|
1170
|
-
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false): Promise<BudgetDecisionResult | undefined> => {
|
|
1447
|
+
const answerBudgetDecision = async (runId: string, proposalId: string, approved: boolean, silent = false, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult | undefined> => {
|
|
1171
1448
|
const run = runs.get(runId);
|
|
1172
1449
|
if (!run) return undefined;
|
|
1173
1450
|
const request = await run.store.answerWorkflowDecision(proposalId, approved);
|
|
1174
1451
|
if (!request) return undefined;
|
|
1175
1452
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1176
|
-
const result = await applyBudgetDecision(request, approved);
|
|
1453
|
+
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
1177
1454
|
if (!silent) deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1178
1455
|
return result;
|
|
1179
1456
|
};
|
|
@@ -1223,16 +1500,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1223
1500
|
label: "Workflow Respond",
|
|
1224
1501
|
description: "Approve or reject one pending workflow checkpoint or budget decision",
|
|
1225
1502
|
parameters: Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
|
|
1226
|
-
async execute(_id, params) {
|
|
1503
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1227
1504
|
try {
|
|
1228
1505
|
if (params.proposalId) {
|
|
1229
|
-
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved);
|
|
1506
|
+
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
1230
1507
|
if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { content: [{ type: "text" as const, text: JSON.stringify(denied) }], details: denied }; }
|
|
1231
1508
|
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: { ...result, reason: params.approved ? "approved" : "rejected" } };
|
|
1232
1509
|
}
|
|
1233
1510
|
if (!params.name) throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
1234
1511
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
1235
|
-
return { content: [{ type: "text" as const, text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response." }], details: { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" }
|
|
1512
|
+
return { content: [{ type: "text" as const, text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response." }], details: { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" } };
|
|
1236
1513
|
} catch (error) {
|
|
1237
1514
|
throw mainAgentError(error);
|
|
1238
1515
|
}
|
|
@@ -1254,47 +1531,60 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1254
1531
|
});
|
|
1255
1532
|
let catalogRegistered = false;
|
|
1256
1533
|
let sessionStarted = false;
|
|
1257
|
-
const registerCatalog = () => {
|
|
1534
|
+
const registerCatalog = (cwd: string, trustedProject: boolean) => {
|
|
1258
1535
|
if (catalogRegistered || !pi.getActiveTools().includes("workflow")) return;
|
|
1259
|
-
const catalog = registry.catalog();
|
|
1260
|
-
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
1261
|
-
|
|
1536
|
+
const catalog = registry.catalog({ cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) });
|
|
1537
|
+
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0 || Boolean(catalog.modelAliasEntries?.length);
|
|
1538
|
+
const hasSettings = catalog.settings !== undefined && [catalog.settings.globalSettingsPath, catalog.settings.projectSettingsPath].some((path) => existsSync(path));
|
|
1539
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases && !hasSettings) return;
|
|
1262
1540
|
pi.registerTool({
|
|
1263
1541
|
name: "workflow_catalog",
|
|
1264
1542
|
label: "Workflow Catalog",
|
|
1265
1543
|
description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
|
|
1266
|
-
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or
|
|
1544
|
+
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
1267
1545
|
async execute(_id, params = {}) {
|
|
1268
|
-
const
|
|
1546
|
+
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
1547
|
+
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
1269
1548
|
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
1270
|
-
}
|
|
1549
|
+
},
|
|
1550
|
+
renderCall(args, theme) {
|
|
1551
|
+
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
1552
|
+
return styledTextBlock(args.name === undefined ? title : `${title} ${theme.fg("accent", args.name)}`);
|
|
1553
|
+
},
|
|
1554
|
+
renderResult(result, options, theme) {
|
|
1555
|
+
return workflowCatalogBlock(formatWorkflowCatalog(catalogResultValue(result), options.expanded, theme), options.expanded);
|
|
1556
|
+
},
|
|
1271
1557
|
});
|
|
1272
1558
|
catalogRegistered = true;
|
|
1273
1559
|
};
|
|
1274
|
-
const refreshPausedRunAliases = async (run: NonNullable<ReturnType<typeof runs.get>>, context?: { model: { provider: string; id: string } | undefined; modelRegistry:
|
|
1560
|
+
const refreshPausedRunAliases = async (run: NonNullable<ReturnType<typeof runs.get>>, context?: { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; projectTrusted?: boolean }) => {
|
|
1275
1561
|
const loaded = await run.store.load();
|
|
1276
1562
|
const active = new Set(pi.getActiveTools().filter((tool) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(tool)));
|
|
1277
1563
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1278
1564
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1279
1565
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
const
|
|
1566
|
+
const trustedProject = context?.projectTrusted ?? run.projectTrusted();
|
|
1567
|
+
const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
|
|
1568
|
+
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1569
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1283
1570
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1571
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1572
|
+
const inventory = modelInventory(rootModel, context?.modelRegistry);
|
|
1573
|
+
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1574
|
+
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1575
|
+
const currentAliases = (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: run.abortController.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
1288
1576
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1289
1577
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1290
|
-
const
|
|
1578
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1579
|
+
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1291
1580
|
await run.store.saveSnapshot(snapshot);
|
|
1292
|
-
|
|
1581
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1582
|
+
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), 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(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(currentPolicy) }, createSession);
|
|
1293
1583
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
1294
1584
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1295
1585
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1296
1586
|
};
|
|
1297
|
-
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:
|
|
1587
|
+
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>> }) => {
|
|
1298
1588
|
const loaded = await run.store.load();
|
|
1299
1589
|
await run.store.validateRetrySource();
|
|
1300
1590
|
await run.store.validateBorrowedWorktrees();
|
|
@@ -1307,26 +1597,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1307
1597
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1308
1598
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1309
1599
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1310
|
-
const
|
|
1311
|
-
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1312
|
-
const
|
|
1600
|
+
const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
|
|
1601
|
+
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1602
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1313
1603
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1604
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1605
|
+
const inventory = modelInventory(rootModel, context?.modelRegistry);
|
|
1606
|
+
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1607
|
+
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1608
|
+
const controller = new AbortController();
|
|
1609
|
+
if (context?.signal?.aborted) controller.abort(); else { context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true }); }
|
|
1610
|
+
run.abortController = controller;
|
|
1611
|
+
const currentAliases = context?.resolvedAliases ?? (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: controller.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
1318
1612
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1319
|
-
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1320
|
-
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1613
|
+
const blockedAliases = context?.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1614
|
+
const blockedAliasTargets = context?.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1321
1615
|
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1322
|
-
preflight(script, { models:
|
|
1323
|
-
const
|
|
1616
|
+
preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
1617
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1618
|
+
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1324
1619
|
await run.store.saveSnapshot(snapshot);
|
|
1325
|
-
|
|
1620
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1621
|
+
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), 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(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(currentPolicy) }, createSession);
|
|
1326
1622
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1327
1623
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1328
|
-
const controller = new AbortController();
|
|
1329
|
-
run.abortController = controller;
|
|
1330
1624
|
const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
|
|
1331
1625
|
run.executor.setRunContext(runContext);
|
|
1332
1626
|
let variables: Readonly<Record<string, JsonValue>>;
|
|
@@ -1388,7 +1682,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1388
1682
|
run.completion = completion;
|
|
1389
1683
|
void completion;
|
|
1390
1684
|
};
|
|
1391
|
-
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean): Promise<BudgetDecisionResult> => {
|
|
1685
|
+
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean, context?: unknown, signal?: AbortSignal): Promise<BudgetDecisionResult> => {
|
|
1392
1686
|
const run = runs.get(request.runId);
|
|
1393
1687
|
if (!run) throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
1394
1688
|
if (!approved) return { state: "budget_exhausted", approved: false };
|
|
@@ -1397,10 +1691,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1397
1691
|
const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
|
|
1398
1692
|
run.budget = runtime;
|
|
1399
1693
|
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; });
|
|
1400
|
-
await coldResumeRun(run, false, {},
|
|
1694
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1401
1695
|
return { state: "running", approved: true };
|
|
1402
1696
|
};
|
|
1403
|
-
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown): Promise<Record<string, JsonValue>> => {
|
|
1697
|
+
const resumeWorkflowRun = async (runId: string, rawPatch?: unknown, context?: unknown, signal?: AbortSignal): Promise<Record<string, JsonValue>> => {
|
|
1404
1698
|
const run = runs.get(runId);
|
|
1405
1699
|
if (!run) throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${runId}`);
|
|
1406
1700
|
const loaded = await run.store.load();
|
|
@@ -1425,11 +1719,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1425
1719
|
run.budget = runtime;
|
|
1426
1720
|
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; });
|
|
1427
1721
|
}
|
|
1428
|
-
await coldResumeRun(run, false, {},
|
|
1722
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1429
1723
|
return { state: "running" };
|
|
1430
1724
|
};
|
|
1431
1725
|
const retryReservations = new Set<string>();
|
|
1432
|
-
const retryWorkflowRun = async (runId: string, context: unknown): Promise<{ runId: string; parentRunId: string; state: "running" }> => {
|
|
1726
|
+
const retryWorkflowRun = async (runId: string, context: unknown, signal?: AbortSignal): Promise<{ runId: string; parentRunId: string; state: "running" }> => {
|
|
1433
1727
|
if (typeof runId !== "string" || !runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
1434
1728
|
const host = object(context) ? context : {};
|
|
1435
1729
|
const cwd = typeof host.cwd === "string" ? host.cwd : undefined;
|
|
@@ -1470,18 +1764,23 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1470
1764
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
1471
1765
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1472
1766
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1473
|
-
const
|
|
1474
|
-
resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
1475
|
-
const
|
|
1767
|
+
const resolution = resolveWorkflowSettings(cwd, trustedProject, settingsPath);
|
|
1768
|
+
const currentPolicy = resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
1769
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1476
1770
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1477
1771
|
const modelRegistry = contextHostCapabilities(context).modelRegistry;
|
|
1478
|
-
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
|
|
1479
1772
|
const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1773
|
+
const rootModel: ModelSpec = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
1774
|
+
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1775
|
+
const knownModels = modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1776
|
+
const availableModels = modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1777
|
+
const resolvedAliases = await resolveLaunchAliases(registry, staticAliases, { cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: signal ?? new AbortController().signal }, availableModels, knownModels, settingsPath);
|
|
1778
|
+
const currentAliases = resolvedAliases.aliases;
|
|
1482
1779
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1780
|
+
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1781
|
+
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1483
1782
|
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1484
|
-
preflight(script, { models:
|
|
1783
|
+
preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
|
|
1485
1784
|
await sourceStore.validateNamedWorktrees();
|
|
1486
1785
|
for (const name of loaded.run.retry?.namedWorktrees ?? []) await sourceStore.resolveNamedWorktree(name);
|
|
1487
1786
|
const completedPaths = (await sourceStore.replayableOperations()).map(({ path }) => path);
|
|
@@ -1490,7 +1789,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1490
1789
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1491
1790
|
const childRunId = randomUUID();
|
|
1492
1791
|
const childStore = new RunStore(cwd, sessionId, childRunId, home);
|
|
1493
|
-
const
|
|
1792
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1793
|
+
const childSnapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1494
1794
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
1495
1795
|
const childInitialBudget = childBudget.snapshot();
|
|
1496
1796
|
const retry: WorkflowRetryProvenance = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
@@ -1499,13 +1799,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1499
1799
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
|
|
1500
1800
|
const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
|
|
1501
1801
|
const abortController = new AbortController();
|
|
1502
|
-
const providerErrorRecovery = createProviderErrorRecovery(context,
|
|
1802
|
+
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
1503
1803
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1504
|
-
const childRun = { executor: new WorkflowAgentExecutor({ cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => active.has(tool) || tool === "workflow_catalog")), availableModels
|
|
1804
|
+
const childRun = { executor: new WorkflowAgentExecutor({ cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => active.has(tool) || tool === "workflow_catalog")), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(currentPolicy) }, createSession), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
|
|
1505
1805
|
runs.set(childRunId, childRun);
|
|
1506
1806
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
1507
1807
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
1508
|
-
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry
|
|
1808
|
+
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
|
|
1509
1809
|
const completion = runs.get(childRunId)?.completion;
|
|
1510
1810
|
if (completion) {
|
|
1511
1811
|
childStarted = true;
|
|
@@ -1521,8 +1821,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1521
1821
|
label: "Workflow Retry",
|
|
1522
1822
|
description: "Retry a failed workflow run by replaying its completed structural operations",
|
|
1523
1823
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
1524
|
-
async execute(_id, params,
|
|
1525
|
-
try { const result = await retryWorkflowRun(params.runId, ctx); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
1824
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1825
|
+
try { const result = await retryWorkflowRun(params.runId, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
1526
1826
|
catch (error) { throw mainAgentError(error); }
|
|
1527
1827
|
},
|
|
1528
1828
|
});
|
|
@@ -1531,8 +1831,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1531
1831
|
label: "Workflow Resume",
|
|
1532
1832
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
1533
1833
|
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
1534
|
-
async execute(_id, params) {
|
|
1535
|
-
try { const result = await resumeWorkflowRun(params.runId, params.budget); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
1834
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1835
|
+
try { const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal); return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result }; }
|
|
1536
1836
|
catch (error) { throw mainAgentError(error); }
|
|
1537
1837
|
},
|
|
1538
1838
|
});
|
|
@@ -1540,7 +1840,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1540
1840
|
if (sessionStarted) return;
|
|
1541
1841
|
sessionStarted = true;
|
|
1542
1842
|
registry.freeze();
|
|
1543
|
-
registerCatalog();
|
|
1843
|
+
registerCatalog(ctx.cwd, projectTrusted(ctx));
|
|
1544
1844
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1545
1845
|
try {
|
|
1546
1846
|
for (const runId of await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)) {
|
|
@@ -1565,7 +1865,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1565
1865
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
1566
1866
|
const abortController = new AbortController();
|
|
1567
1867
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
1568
|
-
runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks()
|
|
1868
|
+
runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir))) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
|
|
1569
1869
|
for (const checkpoint of await store.awaitingCheckpoints()) deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
1570
1870
|
for (const decision of await store.pendingWorkflowDecisions()) deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
1571
1871
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
@@ -1590,7 +1890,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1590
1890
|
});
|
|
1591
1891
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1592
1892
|
if (!pi.getActiveTools().includes("workflow")) return;
|
|
1593
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
1893
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
|
|
1594
1894
|
if (!roles.length) return;
|
|
1595
1895
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
1596
1896
|
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
@@ -1605,27 +1905,29 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1605
1905
|
try {
|
|
1606
1906
|
const headless = object(ctx) && ctx.headless === true;
|
|
1607
1907
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1608
|
-
const defaults = loadSettings(settingsPath);
|
|
1609
1908
|
if (!ctx.model) throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
1610
|
-
const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
|
|
1611
1909
|
const budget = validateBudget(params.budget);
|
|
1612
1910
|
const rootModel: ModelSpec = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
1613
1911
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
1614
1912
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
1615
|
-
const
|
|
1616
|
-
knownModels.
|
|
1617
|
-
const availableModels =
|
|
1913
|
+
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1914
|
+
const knownModels = inventory.knownModels;
|
|
1915
|
+
const availableModels = inventory.availableModels;
|
|
1618
1916
|
const rootTools = pi.getActiveTools().filter((name) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(name));
|
|
1619
1917
|
const trustedProject = projectTrusted(ctx);
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1918
|
+
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1919
|
+
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
1920
|
+
const runController = new AbortController();
|
|
1921
|
+
if (signal?.aborted) runController.abort(); else signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
|
|
1922
|
+
const resolvedAliases = await resolveLaunchAliases(registry, launch.settings.modelAliases ?? {}, { cwd: launchCwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: runController.signal }, availableModels, knownModels, settingsPath);
|
|
1923
|
+
const modelAliases = resolvedAliases.aliases;
|
|
1924
|
+
const settings = Object.freeze({ ...launch.settings, ...(Object.keys(modelAliases).length ? { modelAliases } : {}) });
|
|
1925
|
+
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath }, registry);
|
|
1622
1926
|
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
1623
1927
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1624
1928
|
const runId = randomUUID();
|
|
1625
1929
|
const args = (params.args ?? null) as JsonValue;
|
|
1626
1930
|
encoded(args);
|
|
1627
|
-
const runController = new AbortController();
|
|
1628
|
-
if (signal?.aborted) runController.abort(); else signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
|
|
1629
1931
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
1630
1932
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
1631
1933
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
@@ -1633,9 +1935,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1633
1935
|
if (parentRunId !== undefined) await store.validateParentRun(parentRunId);
|
|
1634
1936
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]])) as Record<string, AgentDefinition>;
|
|
1635
1937
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
1636
|
-
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model,
|
|
1938
|
+
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
1637
1939
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
1638
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function" as const, functionName } : {}), ...(
|
|
1940
|
+
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 });
|
|
1639
1941
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
1640
1942
|
const initialBudget = budgetRuntime.snapshot();
|
|
1641
1943
|
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);
|
|
@@ -1643,7 +1945,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1643
1945
|
const background = !params.foreground;
|
|
1644
1946
|
const providerPause = async () => { if (background) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1645
1947
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
1646
|
-
const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases
|
|
1948
|
+
const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy), runContext }, createSession);
|
|
1647
1949
|
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 } : {}) });
|
|
1648
1950
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1649
1951
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
@@ -1754,7 +2056,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1754
2056
|
return entries.filter((entry): entry is { store: RunStore; loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> } } => entry !== undefined);
|
|
1755
2057
|
};
|
|
1756
2058
|
let stores = await loadStores();
|
|
1757
|
-
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or
|
|
2059
|
+
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint-name]. Approve/reject are for checkpoints only; use workflow_respond with a proposalId or the navigator's budget controls for budget decisions. Use workflow_resume for budget patches."
|
|
1758
2060
|
const setWorkflowStatus = (text: string | undefined) => {
|
|
1759
2061
|
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
1760
2062
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
@@ -1771,7 +2073,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1771
2073
|
return keepContext ? "dashboard" : "done";
|
|
1772
2074
|
}
|
|
1773
2075
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
1774
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true);
|
|
2076
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
1775
2077
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
1776
2078
|
return keepContext ? "dashboard" : "done";
|
|
1777
2079
|
}
|
|
@@ -1784,12 +2086,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1784
2086
|
if (action === "resume" && run) {
|
|
1785
2087
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
1786
2088
|
const patch: unknown = rest.length ? JSON.parse(rest.join(" ")) as unknown : undefined;
|
|
1787
|
-
const result = await resumeWorkflowRun(run.store.runId, patch);
|
|
2089
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
1788
2090
|
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
|
|
1789
2091
|
} else {
|
|
1790
2092
|
if (run.lifecycle.state === "interrupted") await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
1791
2093
|
else {
|
|
1792
|
-
if (run.lifecycle.state === "paused") await refreshPausedRunAliases(run, ctx);
|
|
2094
|
+
if (run.lifecycle.state === "paused") await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
1793
2095
|
await run.lifecycle.resume();
|
|
1794
2096
|
}
|
|
1795
2097
|
ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
|
|
@@ -1799,7 +2101,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1799
2101
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
1800
2102
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}" );
|
|
1801
2103
|
if (input === undefined) return keepContext ? "dashboard" : "done";
|
|
1802
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
2104
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
1803
2105
|
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
|
|
1804
2106
|
return keepContext ? "dashboard" : "done";
|
|
1805
2107
|
}
|
|
@@ -1824,6 +2126,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1824
2126
|
};
|
|
1825
2127
|
const manageAliases = async (): Promise<void> => {
|
|
1826
2128
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2129
|
+
let aliasSettingsPath = settingsPath;
|
|
2130
|
+
const trustedProject = projectTrusted(ctx);
|
|
1827
2131
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
1828
2132
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
1829
2133
|
const selectTarget = async (aliases: Readonly<Record<string, string>>): Promise<string | undefined> => {
|
|
@@ -1834,13 +2138,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1834
2138
|
return (await ctx.ui.input("Manual model ID", "provider/model[:thinking] or alias[:thinking]"))?.trim() || undefined;
|
|
1835
2139
|
};
|
|
1836
2140
|
const save = (aliases: Readonly<Record<string, string>>): boolean => {
|
|
1837
|
-
try { saveModelAliases(
|
|
1838
|
-
catch (error) { ctx.ui.notify(`${
|
|
2141
|
+
try { saveModelAliases(aliasSettingsPath, aliases); ctx.ui.notify(`Saved model aliases to ${aliasSettingsPath}.`, "info"); return true; }
|
|
2142
|
+
catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error"); return false; }
|
|
1839
2143
|
};
|
|
1840
2144
|
for (;;) {
|
|
1841
2145
|
let aliases: Readonly<Record<string, string>>;
|
|
1842
|
-
try {
|
|
1843
|
-
catch (error) { ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error"); return; }
|
|
2146
|
+
try { const resolution = resolveWorkflowSettings(ctx.cwd, trustedProject, settingsPath); aliases = resolution.effective.modelAliases ?? {}; aliasSettingsPath = resolution.sources.modelAliases; }
|
|
2147
|
+
catch (error) { ctx.ui.notify(`${trustedProject ? workflowProjectSettingsPath(ctx.cwd) : settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error"); return; }
|
|
1844
2148
|
const names = Object.keys(aliases).sort();
|
|
1845
2149
|
const listing = names.length ? names.map((name) => `${name} = ${aliases[name] ?? ""}`).join("\n") : "(none)";
|
|
1846
2150
|
const options = ["Add alias", ...names.map((name) => `Edit ${name}`), ...names.map((name) => `Delete ${name}`), "Back"];
|
|
@@ -1853,8 +2157,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1853
2157
|
const target = await selectTarget(aliases);
|
|
1854
2158
|
if (!target) continue;
|
|
1855
2159
|
const next = { ...aliases, [name]: target };
|
|
1856
|
-
try { validateModelAliases(next,
|
|
1857
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2160
|
+
try { validateModelAliases(next, aliasSettingsPath); } catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error"); continue; }
|
|
2161
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
1858
2162
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
1859
2163
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
1860
2164
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?")) continue;
|
|
@@ -1867,8 +2171,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1867
2171
|
const target = await selectTarget(aliases);
|
|
1868
2172
|
if (!target) continue;
|
|
1869
2173
|
const next = { ...aliases, [edit[1]]: target };
|
|
1870
|
-
try { validateModelAliases(next,
|
|
1871
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2174
|
+
try { validateModelAliases(next, aliasSettingsPath); } catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error"); continue; }
|
|
2175
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
1872
2176
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
1873
2177
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
1874
2178
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?")) continue;
|
|
@@ -1899,7 +2203,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1899
2203
|
const labels = navigatorRunLabels(sorted);
|
|
1900
2204
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
1901
2205
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
1902
|
-
const
|
|
2206
|
+
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
2207
|
+
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
1903
2208
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
1904
2209
|
if (!runChoice || runChoice === "Close") return;
|
|
1905
2210
|
if (runChoice === "Inspect session in pane") {
|
|
@@ -1919,6 +2224,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1919
2224
|
}
|
|
1920
2225
|
ctx.ui.notify("Deleted all completed workflow runs.", "info"); stores = await loadStores(); continue;
|
|
1921
2226
|
}
|
|
2227
|
+
if (runChoice === "Delete all failed") {
|
|
2228
|
+
if (!await ctx.ui.confirm("Delete failed runs?", "Delete all failed workflow runs and their artifacts? This cannot be undone.")) continue;
|
|
2229
|
+
for (const entry of sorted) {
|
|
2230
|
+
if (entry.loaded.run.state === "failed") { await entry.store.delete(true); runs.delete(entry.store.runId); terminalRunStates.delete(entry.store.runId); }
|
|
2231
|
+
}
|
|
2232
|
+
ctx.ui.notify("Deleted all failed workflow runs.", "info"); stores = await loadStores(); continue;
|
|
2233
|
+
}
|
|
1922
2234
|
const runIndex = labels.indexOf(runChoice);
|
|
1923
2235
|
if (runIndex < 0) return;
|
|
1924
2236
|
const selected = sorted[runIndex];
|
|
@@ -1968,19 +2280,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
1968
2280
|
addCopy("Copy run path", store.directory, "run path");
|
|
1969
2281
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
1970
2282
|
}
|
|
1971
|
-
return { dashboard:
|
|
2283
|
+
return { dashboard: formatWorkflowPhaseDashboard(loaded.run, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(loaded.run, loaded.snapshot), run: loaded.run, snapshot: loaded.snapshot, actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
|
|
1972
2284
|
};
|
|
1973
2285
|
const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>): Promise<void> => {
|
|
1974
2286
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
1975
|
-
const title = (agent: AgentRecord): string =>
|
|
1976
|
-
const parents: string[] = [];
|
|
1977
|
-
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
1978
|
-
const parent = byId.get(parentId);
|
|
1979
|
-
if (!parent) break;
|
|
1980
|
-
parents.unshift(parent.label ?? parent.name);
|
|
1981
|
-
}
|
|
1982
|
-
return [...((agent.structuralPath ?? []).length ? [(agent.structuralPath ?? []).join(" > ")] : []), ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : []), ...parents, agent.label ?? agent.name].join(" > ");
|
|
1983
|
-
};
|
|
2287
|
+
const title = (agent: AgentRecord): string => agentBreadcrumb(agent, byId, true);
|
|
1984
2288
|
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
1985
2289
|
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
1986
2290
|
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
@@ -2031,8 +2335,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2031
2335
|
let disposed = false;
|
|
2032
2336
|
let stopRequested = false;
|
|
2033
2337
|
let stopStatus: string | undefined;
|
|
2338
|
+
const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
|
|
2339
|
+
let selectedPhaseId = initialSelection.phaseId;
|
|
2340
|
+
let selectedAgentId = initialSelection.agentId;
|
|
2034
2341
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
2035
|
-
const keyLabels: Record<string, string> = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
2342
|
+
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", tab: "tab", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
2036
2343
|
const keyLabel = (binding: string, fallback: string) => {
|
|
2037
2344
|
const keys = keybindingKeys(keybindings, binding);
|
|
2038
2345
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
@@ -2046,11 +2353,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2046
2353
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
2047
2354
|
};
|
|
2048
2355
|
const updateDashboard = async (selectedOption: string | undefined) => {
|
|
2356
|
+
const phaseId = selectedPhaseId;
|
|
2357
|
+
const agentId = selectedAgentId;
|
|
2049
2358
|
const next = await loadDashboard();
|
|
2050
2359
|
if (disposed) return;
|
|
2051
2360
|
view = next;
|
|
2052
2361
|
options = [...view.actions.keys(), "Close"];
|
|
2053
2362
|
selectedIndex = Math.max(0, options.indexOf(selectedOption ?? ""));
|
|
2363
|
+
const preserved = preserveWorkflowPhaseSelection(view.phaseModel, { phaseId, agentId });
|
|
2364
|
+
selectedPhaseId = preserved.phaseId;
|
|
2365
|
+
selectedAgentId = preserved.agentId;
|
|
2054
2366
|
tui.requestRender();
|
|
2055
2367
|
};
|
|
2056
2368
|
const requestStop = () => {
|
|
@@ -2081,11 +2393,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2081
2393
|
timer.unref();
|
|
2082
2394
|
return borderWorkflowOverlay({
|
|
2083
2395
|
render(width: number) {
|
|
2084
|
-
const
|
|
2085
|
-
const
|
|
2396
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
2397
|
+
const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { phaseId: selectedPhaseId, agentId: selectedAgentId }, styles);
|
|
2398
|
+
const dashboardLines = stopStatus ? [styles.error(stopStatus), ...phaseLines] : phaseLines;
|
|
2086
2399
|
const actionLines = options.map((option, index) => truncateToVisualLines(`${index === selectedIndex ? "→ " : " "}${option}`, Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "");
|
|
2087
2400
|
const layout = dashboardLayout();
|
|
2088
|
-
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}
|
|
2401
|
+
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} phases · ${keyLabel("tui.input.tab", "tab")} agents${dashboardLines.length > layout.dashboardViewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · ${keyLabel("tui.select.confirm", "enter")} select · ${keyLabel("tui.select.cancel", "esc")} close · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
2089
2402
|
const compact = [...dashboardLines, "", ...actionLines, "", hint];
|
|
2090
2403
|
if (compact.length <= layout.rows) { dashboardOffset = 0; return compact; }
|
|
2091
2404
|
const maxOffset = Math.max(0, dashboardLines.length - layout.dashboardViewport);
|
|
@@ -2101,7 +2414,27 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2101
2414
|
invalidate() {},
|
|
2102
2415
|
handleInput(data: string) {
|
|
2103
2416
|
if (stopRequested) return;
|
|
2104
|
-
|
|
2417
|
+
const currentPhase = () => view.phaseModel.phases.find((phase) => phase.id === selectedPhaseId);
|
|
2418
|
+
const left = keybindings.matches(data, "tui.editor.cursorLeft");
|
|
2419
|
+
const right = keybindings.matches(data, "tui.editor.cursorRight");
|
|
2420
|
+
if (left || right) {
|
|
2421
|
+
const phases = view.phaseModel.phases;
|
|
2422
|
+
if (phases.length) {
|
|
2423
|
+
const currentIndex = Math.max(0, phases.findIndex((phase) => phase.id === selectedPhaseId));
|
|
2424
|
+
const delta = left ? -1 : 1;
|
|
2425
|
+
const nextPhase = phases[(currentIndex + delta + phases.length) % phases.length];
|
|
2426
|
+
selectedPhaseId = nextPhase?.id;
|
|
2427
|
+
selectedAgentId = nextPhase?.agents[0]?.id;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
else if (keybindings.matches(data, "tui.input.tab")) {
|
|
2431
|
+
const agents = currentPhase()?.agents ?? [];
|
|
2432
|
+
if (agents.length) {
|
|
2433
|
+
const currentIndex = Math.max(0, agents.findIndex((agent) => agent.id === selectedAgentId));
|
|
2434
|
+
selectedAgentId = agents[(currentIndex + 1) % agents.length]?.id;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
else if (keybindings.matches(data, "tui.select.up")) selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
2105
2438
|
else if (keybindings.matches(data, "tui.select.down")) selectedIndex = (selectedIndex + 1) % options.length;
|
|
2106
2439
|
else if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
2107
2440
|
dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, dashboardLayout().dashboardViewport));
|