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/dist/src/host.js
CHANGED
|
@@ -10,13 +10,32 @@ import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } f
|
|
|
10
10
|
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
11
11
|
import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
12
12
|
import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
13
|
-
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
14
|
-
import { launchScriptForSnapshot, loadAgentDefinitions,
|
|
13
|
+
import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelAliasErrorName, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
14
|
+
import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
|
|
15
15
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
|
|
16
16
|
import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
17
17
|
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 } from "./types.js";
|
|
18
18
|
const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
|
|
19
|
+
function snapshotResourcePolicy(snapshot, cwd, projectTrusted, globalSettingsPath) {
|
|
20
|
+
const empty = { skills: [], extensions: [] };
|
|
21
|
+
return { globalSettingsPath, projectSettingsPath: join(cwd, ".pi", "pi-extensible-workflows", "settings.json"), projectTrusted, global: empty, project: empty, effective: snapshot.settings.disabledAgentResources ?? empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
22
|
+
}
|
|
19
23
|
const PLAIN_WORKFLOW_PROGRESS_STYLES = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
24
|
+
function workflowLaunchSettings(cwd, projectTrusted, globalSettingsPath, concurrency) {
|
|
25
|
+
const resolution = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
26
|
+
const settings = Object.freeze({ ...resolution.effective, ...(concurrency === undefined ? {} : { concurrency }) });
|
|
27
|
+
return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath), modelSettingsPath: resolution.sources.modelAliases };
|
|
28
|
+
}
|
|
29
|
+
function frozenResourcePolicy(policy) { return () => structuredClone(policy); }
|
|
30
|
+
function resumedSnapshotSettings(snapshot, resolution, modelAliases) {
|
|
31
|
+
const settings = { ...snapshot.settings, concurrency: snapshot.settingsSources === undefined || snapshot.settingsSources.concurrency === "per-run options" ? snapshot.settings.concurrency : resolution.effective.concurrency, modelAliases };
|
|
32
|
+
if (resolution.effective.disabledAgentResources === undefined)
|
|
33
|
+
delete settings.disabledAgentResources;
|
|
34
|
+
else
|
|
35
|
+
settings.disabledAgentResources = resolution.effective.disabledAgentResources;
|
|
36
|
+
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 };
|
|
37
|
+
return { settings, ...(settingsSources === undefined ? {} : { settingsSources }) };
|
|
38
|
+
}
|
|
20
39
|
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
21
40
|
function workflowDetail(message) {
|
|
22
41
|
const detail = message.trim().replace(new RegExp(`\\b(?:${ERROR_CODES.join("|")})\\b:?\\s*`, "g"), "").replace(/^\s*[A-Z][A-Z0-9_]+:\s*/, "").split("\n").filter((line) => !/^\s*at\s/.test(line)).join("\n").replace(/^Run \S+(?=\s(?:exceeded|is))/i, "Run").replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "the workflow").replace(/^(?:Pi )session \S+(?=\s(?:is|has))/i, "session").replace(/^(Unknown scheduler run|Missing production ownership record|Persisted agent belongs to another run):\s*\S+/i, "$1").replace(/\b(?:runId|sessionId|callSite|occurrence|failedAt|id)[:=]\s*\S+/gi, "").replace(/\s{2,}/g, " ").trim();
|
|
@@ -160,6 +179,116 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
160
179
|
parentRunId: Type.Optional(Type.String({ description: "Terminal run whose named worktrees may be reused" })),
|
|
161
180
|
});
|
|
162
181
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
|
|
182
|
+
function phaseNames(source) {
|
|
183
|
+
const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
|
|
184
|
+
return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
|
|
185
|
+
}
|
|
186
|
+
function phaseAgentCounts(agents) {
|
|
187
|
+
const counts = { total: agents.length, completed: 0, running: 0, failed: 0, cancelled: 0, pending: 0 };
|
|
188
|
+
for (const agent of agents) {
|
|
189
|
+
if (agent.state === "completed")
|
|
190
|
+
counts.completed += 1;
|
|
191
|
+
else if (agent.state === "running")
|
|
192
|
+
counts.running += 1;
|
|
193
|
+
else if (agent.state === "failed")
|
|
194
|
+
counts.failed += 1;
|
|
195
|
+
else if (agent.state === "cancelled")
|
|
196
|
+
counts.cancelled += 1;
|
|
197
|
+
else
|
|
198
|
+
counts.pending += 1;
|
|
199
|
+
}
|
|
200
|
+
return counts;
|
|
201
|
+
}
|
|
202
|
+
function phaseState(runState, counts, isLatest) {
|
|
203
|
+
if (!isLatest)
|
|
204
|
+
return "completed";
|
|
205
|
+
if (runState === "failed")
|
|
206
|
+
return "failed";
|
|
207
|
+
if (runState === "stopped")
|
|
208
|
+
return "cancelled";
|
|
209
|
+
if (runState === "interrupted")
|
|
210
|
+
return "interrupted";
|
|
211
|
+
if (runState === "budget_exhausted")
|
|
212
|
+
return "budget_exhausted";
|
|
213
|
+
if (counts.failed > 0)
|
|
214
|
+
return "failed";
|
|
215
|
+
if (counts.cancelled > 0)
|
|
216
|
+
return "cancelled";
|
|
217
|
+
if (counts.running > 0 || counts.pending > 0)
|
|
218
|
+
return "running";
|
|
219
|
+
return runState === "completed" ? "completed" : "running";
|
|
220
|
+
}
|
|
221
|
+
export function buildWorkflowPhaseModel(run, source) {
|
|
222
|
+
const declaredPhases = phaseNames(source);
|
|
223
|
+
const rawHistory = Array.isArray(run.phaseHistory) ? run.phaseHistory : [];
|
|
224
|
+
const observed = [];
|
|
225
|
+
let boundary = 0;
|
|
226
|
+
for (const record of rawHistory) {
|
|
227
|
+
if (!object(record) || typeof record.phase !== "string" || !record.phase.trim() || typeof record.afterAgent !== "number" || !Number.isSafeInteger(record.afterAgent))
|
|
228
|
+
continue;
|
|
229
|
+
boundary = Math.max(boundary, Math.min(run.agents.length, Math.max(0, record.afterAgent)));
|
|
230
|
+
observed.push({ name: record.phase.trim(), afterAgent: boundary });
|
|
231
|
+
}
|
|
232
|
+
if (!observed.length && typeof run.phase === "string" && run.phase.trim())
|
|
233
|
+
observed.push({ name: run.phase.trim(), afterAgent: 0 });
|
|
234
|
+
const observedEntries = observed.map((entry, index) => ({ ...entry, index, agents: run.agents.slice(entry.afterAgent, observed[index + 1]?.afterAgent ?? run.agents.length) }));
|
|
235
|
+
const matchedDeclarations = new Set();
|
|
236
|
+
const declarationIndices = observedEntries.map((entry) => {
|
|
237
|
+
const index = declaredPhases.findIndex((name, candidate) => !matchedDeclarations.has(candidate) && name === entry.name);
|
|
238
|
+
if (index >= 0)
|
|
239
|
+
matchedDeclarations.add(index);
|
|
240
|
+
return index >= 0 ? index : undefined;
|
|
241
|
+
});
|
|
242
|
+
const entries = observedEntries.map((entry, index) => ({ name: entry.name, observedIndex: index, ...(declarationIndices[index] === undefined ? {} : { declarationIndex: declarationIndices[index] }) }));
|
|
243
|
+
for (const [declarationIndex, name] of declaredPhases.entries()) {
|
|
244
|
+
if (matchedDeclarations.has(declarationIndex))
|
|
245
|
+
continue;
|
|
246
|
+
const insertion = entries.findIndex((entry) => entry.declarationIndex !== undefined && entry.declarationIndex > declarationIndex);
|
|
247
|
+
const pending = { name };
|
|
248
|
+
if (insertion < 0)
|
|
249
|
+
entries.push(pending);
|
|
250
|
+
else
|
|
251
|
+
entries.splice(insertion, 0, pending);
|
|
252
|
+
}
|
|
253
|
+
const occurrences = new Map();
|
|
254
|
+
const phases = entries.map((entry) => {
|
|
255
|
+
const occurrence = (occurrences.get(entry.name) ?? 0) + 1;
|
|
256
|
+
occurrences.set(entry.name, occurrence);
|
|
257
|
+
const observation = entry.observedIndex === undefined ? undefined : observedEntries[entry.observedIndex];
|
|
258
|
+
const agents = observation?.agents ?? [];
|
|
259
|
+
const counts = phaseAgentCounts(agents);
|
|
260
|
+
const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1) : "not started";
|
|
261
|
+
return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, status: state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
|
|
262
|
+
});
|
|
263
|
+
let currentPhaseIndex;
|
|
264
|
+
for (let index = phases.length - 1; index >= 0; index -= 1) {
|
|
265
|
+
if (phases[index]?.observed) {
|
|
266
|
+
currentPhaseIndex = index;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const counts = {};
|
|
271
|
+
for (const phase of phases)
|
|
272
|
+
counts[phase.state] = (counts[phase.state] ?? 0) + 1;
|
|
273
|
+
const current = currentPhaseIndex === undefined ? undefined : phases[currentPhaseIndex];
|
|
274
|
+
const assigned = new Set(observedEntries.flatMap(({ agents }) => agents.map((agent) => agent.id)));
|
|
275
|
+
const unassignedAgents = run.agents.filter((agent) => !assigned.has(agent.id));
|
|
276
|
+
const result = { declaredPhases, phases, counts };
|
|
277
|
+
if (current !== undefined && currentPhaseIndex !== undefined) {
|
|
278
|
+
result.currentPhaseIndex = currentPhaseIndex;
|
|
279
|
+
result.currentPhaseId = current.id;
|
|
280
|
+
}
|
|
281
|
+
if (unassignedAgents.length)
|
|
282
|
+
result.unassignedAgents = unassignedAgents;
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
export function preserveWorkflowPhaseSelection(model, selection) {
|
|
286
|
+
const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
|
|
287
|
+
if (!phase)
|
|
288
|
+
return {};
|
|
289
|
+
const agentId = phase.agents.some((agent) => agent.id === selection.agentId) ? selection.agentId : phase.agents[0]?.id;
|
|
290
|
+
return { phaseId: phase.id, ...(agentId === undefined ? {} : { agentId }) };
|
|
291
|
+
}
|
|
163
292
|
function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
164
293
|
function agentGroupLabel(agents) {
|
|
165
294
|
const structural = agents[0]?.structuralPath ?? [];
|
|
@@ -171,8 +300,13 @@ function agentGroups(agents, allAgents = agents) {
|
|
|
171
300
|
const groups = new Map();
|
|
172
301
|
for (const [index, agent] of agents.entries()) {
|
|
173
302
|
let depth = 0;
|
|
174
|
-
|
|
303
|
+
const seen = new Set([agent.id]);
|
|
304
|
+
for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) {
|
|
305
|
+
if (seen.has(parent))
|
|
306
|
+
break;
|
|
307
|
+
seen.add(parent);
|
|
175
308
|
depth += 1;
|
|
309
|
+
}
|
|
176
310
|
const key = agentGroupKey(agent);
|
|
177
311
|
const group = groups.get(key) ?? { agents: [] };
|
|
178
312
|
group.agents.push({ agent, index, depth });
|
|
@@ -239,6 +373,101 @@ function textBlock(text) {
|
|
|
239
373
|
invalidate() { },
|
|
240
374
|
};
|
|
241
375
|
}
|
|
376
|
+
function styledTextBlock(text) {
|
|
377
|
+
return {
|
|
378
|
+
render(width) {
|
|
379
|
+
return truncateWorkflowProgress(text, width);
|
|
380
|
+
},
|
|
381
|
+
invalidate() { },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function workflowCatalogBlock(text, expanded) {
|
|
385
|
+
return {
|
|
386
|
+
render(width) {
|
|
387
|
+
const safeWidth = Math.max(1, width);
|
|
388
|
+
if (!expanded)
|
|
389
|
+
return truncateWorkflowProgress(text, safeWidth);
|
|
390
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines.map((line) => line.trimEnd());
|
|
391
|
+
},
|
|
392
|
+
invalidate() { },
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
|
|
396
|
+
function catalogResultValue(result) {
|
|
397
|
+
if (result.details !== undefined)
|
|
398
|
+
return result.details;
|
|
399
|
+
const text = result.content?.find((entry) => entry.type === "text")?.text;
|
|
400
|
+
if (!text)
|
|
401
|
+
return undefined;
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(text);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return text;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function isCatalogIndex(value) {
|
|
410
|
+
return object(value) && Array.isArray(value.functions) && Array.isArray(value.variables);
|
|
411
|
+
}
|
|
412
|
+
function isCatalogFunction(value) {
|
|
413
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.input) && object(value.output);
|
|
414
|
+
}
|
|
415
|
+
function isCatalogVariable(value) {
|
|
416
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.schema);
|
|
417
|
+
}
|
|
418
|
+
function isCatalogError(value) {
|
|
419
|
+
return object(value) && object(value.error) && typeof value.error.message === "string";
|
|
420
|
+
}
|
|
421
|
+
function catalogSectionTitle(label, count, theme) {
|
|
422
|
+
return theme.fg("accent", theme.bold(`${label} (${String(count)})`));
|
|
423
|
+
}
|
|
424
|
+
function catalogIndexEntries(entries, theme) {
|
|
425
|
+
const width = Math.max(0, ...entries.map((entry) => entry.name.length));
|
|
426
|
+
return entries.map((entry) => ` ${theme.fg("accent", entry.name.padEnd(width))} ${theme.fg("toolOutput", catalogText(entry.description))}`);
|
|
427
|
+
}
|
|
428
|
+
function formatCatalogIndex(catalog, theme) {
|
|
429
|
+
const aliases = Object.prototype.propertyIsEnumerable.call(catalog, "modelAliases") ? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" })) : catalog.modelAliasEntries ?? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" }));
|
|
430
|
+
const aliasWidth = Math.max(0, ...aliases.map(({ name }) => name.length));
|
|
431
|
+
const aliasLines = aliases.map(({ name, kind, provenance }) => ` ${theme.fg("accent", name.padEnd(aliasWidth))} ${theme.fg("toolOutput", `${kind} · ${provenance}`)}`);
|
|
432
|
+
return [
|
|
433
|
+
catalogSectionTitle("Functions", catalog.functions.length, theme),
|
|
434
|
+
...catalogIndexEntries(catalog.functions, theme),
|
|
435
|
+
"",
|
|
436
|
+
catalogSectionTitle("Variables", catalog.variables.length, theme),
|
|
437
|
+
...catalogIndexEntries(catalog.variables, theme),
|
|
438
|
+
"",
|
|
439
|
+
catalogSectionTitle("Model aliases", aliases.length, theme),
|
|
440
|
+
...aliasLines,
|
|
441
|
+
].join("\n");
|
|
442
|
+
}
|
|
443
|
+
function catalogSchemaLines(schema, theme) {
|
|
444
|
+
const json = JSON.stringify(schema, null, 2);
|
|
445
|
+
return json.split("\n").map((line) => ` ${theme.fg("toolOutput", line)}`);
|
|
446
|
+
}
|
|
447
|
+
function formatCatalogDetail(value, expanded, theme) {
|
|
448
|
+
if ("kind" in value)
|
|
449
|
+
return [theme.fg("accent", theme.bold("Model alias")), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", `${value.kind} · ${value.provenance}`)}`].join("\n");
|
|
450
|
+
const kind = "input" in value ? "Function" : "Variable";
|
|
451
|
+
if (!expanded)
|
|
452
|
+
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");
|
|
453
|
+
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"))];
|
|
454
|
+
if ("input" in value)
|
|
455
|
+
lines.push(theme.fg("muted", "Input schema"), ...catalogSchemaLines(value.input, theme), "", theme.fg("muted", "Output schema"), ...catalogSchemaLines(value.output, theme));
|
|
456
|
+
else
|
|
457
|
+
lines.push(theme.fg("muted", "Variable schema"), ...catalogSchemaLines(value.schema, theme));
|
|
458
|
+
return lines.join("\n");
|
|
459
|
+
}
|
|
460
|
+
function formatWorkflowCatalog(value, expanded, theme) {
|
|
461
|
+
if (isCatalogIndex(value))
|
|
462
|
+
return formatCatalogIndex(value, theme);
|
|
463
|
+
if (isCatalogFunction(value) || isCatalogVariable(value))
|
|
464
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
465
|
+
if (object(value) && typeof value.name === "string" && (value.kind === "static" || value.kind === "dynamic"))
|
|
466
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
467
|
+
if (isCatalogError(value))
|
|
468
|
+
return theme.fg("error", value.error.message);
|
|
469
|
+
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
470
|
+
}
|
|
242
471
|
const ANSI_SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`);
|
|
243
472
|
export function truncateWorkflowProgress(text, width) {
|
|
244
473
|
const safeWidth = Math.max(1, width);
|
|
@@ -263,7 +492,7 @@ function themeWorkflowProgressStyles(theme) {
|
|
|
263
492
|
warning: (text) => theme.fg("warning", text),
|
|
264
493
|
muted: (text) => theme.fg("muted", text),
|
|
265
494
|
dim: (text) => theme.fg("dim", text),
|
|
266
|
-
bold: (text) => theme.bold(text),
|
|
495
|
+
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
267
496
|
};
|
|
268
497
|
}
|
|
269
498
|
function workflowProgressBlock(run, theme) {
|
|
@@ -317,32 +546,33 @@ function navigatorRunLabels(entries) {
|
|
|
317
546
|
return `${glyph} ${run.workflowName}${suffix} ${run.state} ${run.phase ?? ""} ${String(done)}/${String(run.agents.length)} agents${costStr}`;
|
|
318
547
|
});
|
|
319
548
|
}
|
|
320
|
-
function agentBreadcrumbParts(agent, byId) {
|
|
321
|
-
const
|
|
322
|
-
const parts = agent.
|
|
549
|
+
export function agentBreadcrumbParts(agent, byId, includeStructuralPath = false) {
|
|
550
|
+
const leaf = agent.label ?? agent.name;
|
|
551
|
+
const parts = includeStructuralPath && agent.structuralPath?.length ? [agent.structuralPath.join(" > ")] : [];
|
|
552
|
+
if (agent.parentBreadcrumb)
|
|
553
|
+
parts.push(agent.parentBreadcrumb);
|
|
554
|
+
const ancestors = [];
|
|
323
555
|
const seen = new Set([agent.id]);
|
|
324
556
|
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
325
557
|
if (seen.has(parentId))
|
|
326
|
-
break;
|
|
558
|
+
break;
|
|
327
559
|
seen.add(parentId);
|
|
328
560
|
const parent = byId.get(parentId);
|
|
329
|
-
if (parent)
|
|
330
|
-
parts.push(parent.label ?? parent.name);
|
|
331
|
-
else
|
|
561
|
+
if (!parent)
|
|
332
562
|
break;
|
|
563
|
+
ancestors.push(parent.label ?? parent.name);
|
|
333
564
|
}
|
|
334
|
-
parts.push(
|
|
565
|
+
parts.push(...ancestors.reverse(), leaf);
|
|
335
566
|
return parts;
|
|
336
567
|
}
|
|
337
|
-
function agentBreadcrumb(agent, byId) {
|
|
338
|
-
|
|
339
|
-
return parts.length > 1 ? parts.join(" > ") : parts[0] ?? "";
|
|
568
|
+
export function agentBreadcrumb(agent, byId, includeStructuralPath = false) {
|
|
569
|
+
return agentBreadcrumbParts(agent, byId, includeStructuralPath).join(" > ");
|
|
340
570
|
}
|
|
341
571
|
function styledAgentBreadcrumb(agent, byId, styles) {
|
|
342
572
|
const parts = agentBreadcrumbParts(agent, byId);
|
|
343
573
|
if (parts.length <= 1)
|
|
344
574
|
return parts[0] ?? "";
|
|
345
|
-
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${parts[parts.length - 1] ?? ""}`;
|
|
575
|
+
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
346
576
|
}
|
|
347
577
|
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
348
578
|
const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
|
|
@@ -401,6 +631,7 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
401
631
|
`Launch cwd: ${run.cwd}`,
|
|
402
632
|
...formatCompactBudgetStatus(run),
|
|
403
633
|
`Launch models: ${snapshot.models.join(", ") || "(none)"}`,
|
|
634
|
+
`Settings: concurrency=${String(snapshot.settings.concurrency)}`,
|
|
404
635
|
];
|
|
405
636
|
if (run.error)
|
|
406
637
|
lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
@@ -409,6 +640,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
409
640
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
410
641
|
if (aliases && Object.keys(aliases).length)
|
|
411
642
|
lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
|
|
643
|
+
if (snapshot.settingsSources)
|
|
644
|
+
lines.push(`Settings sources: concurrency=${snapshot.settingsSources.concurrency}, modelAliases=${snapshot.settingsSources.modelAliases}, disabledAgentResources=${snapshot.settingsSources.disabledAgentResources}`);
|
|
412
645
|
lines.push("Agents / ownership:");
|
|
413
646
|
if (!run.agents.length)
|
|
414
647
|
lines.push(" (none)");
|
|
@@ -435,6 +668,69 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
435
668
|
lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
|
|
436
669
|
return lines.join("\n");
|
|
437
670
|
}
|
|
671
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
672
|
+
const safeWidth = Math.max(1, width);
|
|
673
|
+
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
674
|
+
const wrap = (text, limit = safeWidth) => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
|
|
675
|
+
const phaseStyle = (state) => 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);
|
|
676
|
+
const phaseName = (phase) => `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`;
|
|
677
|
+
const phaseCounts = (phase) => `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)}`;
|
|
678
|
+
const phaseLine = (phase, selected) => `${selected ? "→" : " "} ${phaseName(phase)} · ${phaseStyle(phase.state)(phase.state)} · ${phaseCounts(phase)}`;
|
|
679
|
+
const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
|
|
680
|
+
const statusSummary = stateNames.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
|
|
681
|
+
const selectedIndex = selection.phaseId ? model.phases.findIndex((phase) => phase.id === selection.phaseId) : -1;
|
|
682
|
+
const activeIndex = selectedIndex >= 0 ? selectedIndex : model.currentPhaseIndex ?? (model.phases.length ? 0 : -1);
|
|
683
|
+
const selectedPhase = activeIndex >= 0 ? model.phases[activeIndex] : undefined;
|
|
684
|
+
const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
685
|
+
if (run.error)
|
|
686
|
+
lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
687
|
+
lines.push(`phase: ${run.phase ?? selectedPhase?.name ?? "none"}`, `Run state: ${run.state}`, `Phases: ${statusSummary}`);
|
|
688
|
+
for (const event of run.events ?? [])
|
|
689
|
+
lines.push(styles.warning(`Warning: ${event.message}`));
|
|
690
|
+
lines.push(...formatCompactBudgetStatus(run));
|
|
691
|
+
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
692
|
+
const renderAgentLines = (agents, selectedAgentId) => {
|
|
693
|
+
if (!agents.length)
|
|
694
|
+
return [styles.muted("Agents: none")];
|
|
695
|
+
const rendered = [];
|
|
696
|
+
for (const agent of agents) {
|
|
697
|
+
const selected = agent.id === selectedAgentId;
|
|
698
|
+
const stateStyle = progressStyleForState(agent.state, styles);
|
|
699
|
+
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
700
|
+
rendered.push(`${selected ? "→" : " "} ${stateStyle(icon)} ${styledAgentBreadcrumb(agent, byId, styles)} · ${stateStyle(agent.state)}`);
|
|
701
|
+
if (agent.state === "failed") {
|
|
702
|
+
const error = agent.attemptDetails?.at(-1)?.error;
|
|
703
|
+
if (error)
|
|
704
|
+
rendered.push(styles.error(` error: ${error.code}: ${error.message}`));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return rendered;
|
|
708
|
+
};
|
|
709
|
+
if (!model.phases.length) {
|
|
710
|
+
lines.push(styles.muted("Phases: no declared or observed phases (legacy snapshot)"), styles.bold("Agents"), ...renderAgentLines(run.agents, selection.agentId));
|
|
711
|
+
return lines.flatMap((line) => wrap(line));
|
|
712
|
+
}
|
|
713
|
+
const selectedAgent = selectedPhase?.agents.find((agent) => agent.id === selection.agentId) ?? selectedPhase?.agents[0];
|
|
714
|
+
if (safeWidth >= 80) {
|
|
715
|
+
const sidebarWidth = Math.min(38, Math.max(24, Math.floor(safeWidth * 0.36)));
|
|
716
|
+
const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
|
|
717
|
+
const sidebar = [styles.bold("Phases"), ...model.phases.flatMap((phase, index) => wrap(phaseLine(phase, index === activeIndex), sidebarWidth))];
|
|
718
|
+
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")];
|
|
719
|
+
const rows = Math.max(sidebar.length, detail.length);
|
|
720
|
+
for (let index = 0; index < rows; index += 1)
|
|
721
|
+
lines.push(`${sidebar[index] ?? ""} | ${detail[index] ?? ""}`);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
lines.push(styles.bold("Phases"));
|
|
725
|
+
for (const [index, phase] of model.phases.entries())
|
|
726
|
+
lines.push(...wrap(phaseLine(phase, index === activeIndex)));
|
|
727
|
+
if (selectedPhase)
|
|
728
|
+
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)));
|
|
729
|
+
}
|
|
730
|
+
if (model.unassignedAgents?.length)
|
|
731
|
+
lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
732
|
+
return lines.flatMap((line) => wrap(line));
|
|
733
|
+
}
|
|
438
734
|
function formatCheckpointReview(checkpoint) {
|
|
439
735
|
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
440
736
|
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
@@ -938,6 +1234,38 @@ function contextHostCapabilities(ctx) {
|
|
|
938
1234
|
const getAvailable = registry.getAvailable;
|
|
939
1235
|
return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
940
1236
|
}
|
|
1237
|
+
function modelInventory(root, registry) {
|
|
1238
|
+
const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
1239
|
+
const available = registry?.getAvailable?.() ?? registry?.getAll?.() ?? [];
|
|
1240
|
+
const knownModels = new Set(all.map((model) => `${model.provider}/${model.id}`));
|
|
1241
|
+
const availableModels = new Set(available.map((model) => `${model.provider}/${model.id}`));
|
|
1242
|
+
const rootName = root?.provider && root.model ? `${root.provider}/${root.model}` : undefined;
|
|
1243
|
+
if (rootName) {
|
|
1244
|
+
knownModels.add(rootName);
|
|
1245
|
+
availableModels.add(rootName);
|
|
1246
|
+
}
|
|
1247
|
+
return { knownModels, availableModels };
|
|
1248
|
+
}
|
|
1249
|
+
function resumeHostContext(ctx) {
|
|
1250
|
+
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;
|
|
1251
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry };
|
|
1252
|
+
}
|
|
1253
|
+
async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
|
|
1254
|
+
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
1255
|
+
const dynamicNames = Object.keys(dynamic);
|
|
1256
|
+
try {
|
|
1257
|
+
const aliases = validateModelAliases({ ...dynamic, ...staticAliases }, settingsPath);
|
|
1258
|
+
validateModelAliasAvailability(aliases, dynamicNames, availableModels, knownModels, settingsPath);
|
|
1259
|
+
return { aliases, dynamicNames };
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
const name = modelAliasErrorName(error);
|
|
1263
|
+
const descriptor = name && typeof registry.modelAliases === "function" ? registry.modelAliases().find((candidate) => candidate.name === name) : undefined;
|
|
1264
|
+
if (descriptor && errorCode(error) !== "CANCELLED")
|
|
1265
|
+
throw new WorkflowError(errorCode(error) ?? "CONFIG_ERROR", `${errorText(error)} (extension: ${descriptor.headline})`);
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
941
1269
|
function isUiSelect(value) { return typeof value === "function"; }
|
|
942
1270
|
function isUiInput(value) { return typeof value === "function"; }
|
|
943
1271
|
function isUiSetStatus(value) { return typeof value === "function"; }
|
|
@@ -1084,11 +1412,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1084
1412
|
};
|
|
1085
1413
|
const phaseBridge = (store, metadata, lifecycle) => {
|
|
1086
1414
|
let cursor = 0;
|
|
1087
|
-
let executionPhase;
|
|
1088
1415
|
return async (phase) => {
|
|
1089
|
-
if (phase === executionPhase)
|
|
1090
|
-
return;
|
|
1091
|
-
executionPhase = phase;
|
|
1092
1416
|
await scheduler.flush();
|
|
1093
1417
|
await lifecycle.enter();
|
|
1094
1418
|
try {
|
|
@@ -1293,7 +1617,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1293
1617
|
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) });
|
|
1294
1618
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
1295
1619
|
};
|
|
1296
|
-
const answerBudgetDecision = async (runId, proposalId, approved, silent = false) => {
|
|
1620
|
+
const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
|
|
1297
1621
|
const run = runs.get(runId);
|
|
1298
1622
|
if (!run)
|
|
1299
1623
|
return undefined;
|
|
@@ -1301,7 +1625,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1301
1625
|
if (!request)
|
|
1302
1626
|
return undefined;
|
|
1303
1627
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
1304
|
-
const result = await applyBudgetDecision(request, approved);
|
|
1628
|
+
const result = await applyBudgetDecision(request, approved, context, signal);
|
|
1305
1629
|
if (!silent)
|
|
1306
1630
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
1307
1631
|
return result;
|
|
@@ -1362,10 +1686,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1362
1686
|
label: "Workflow Respond",
|
|
1363
1687
|
description: "Approve or reject one pending workflow checkpoint or budget decision",
|
|
1364
1688
|
parameters: Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
|
|
1365
|
-
async execute(_id, params) {
|
|
1689
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1366
1690
|
try {
|
|
1367
1691
|
if (params.proposalId) {
|
|
1368
|
-
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved);
|
|
1692
|
+
const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
1369
1693
|
if (!result) {
|
|
1370
1694
|
const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
|
|
1371
1695
|
return { content: [{ type: "text", text: JSON.stringify(denied) }], details: denied };
|
|
@@ -1399,22 +1723,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1399
1723
|
});
|
|
1400
1724
|
let catalogRegistered = false;
|
|
1401
1725
|
let sessionStarted = false;
|
|
1402
|
-
const registerCatalog = () => {
|
|
1726
|
+
const registerCatalog = (cwd, trustedProject) => {
|
|
1403
1727
|
if (catalogRegistered || !pi.getActiveTools().includes("workflow"))
|
|
1404
1728
|
return;
|
|
1405
|
-
const catalog = registry.catalog();
|
|
1406
|
-
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
1407
|
-
|
|
1729
|
+
const catalog = registry.catalog({ cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) });
|
|
1730
|
+
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0 || Boolean(catalog.modelAliasEntries?.length);
|
|
1731
|
+
const hasSettings = catalog.settings !== undefined && [catalog.settings.globalSettingsPath, catalog.settings.projectSettingsPath].some((path) => existsSync(path));
|
|
1732
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases && !hasSettings)
|
|
1408
1733
|
return;
|
|
1409
1734
|
pi.registerTool({
|
|
1410
1735
|
name: "workflow_catalog",
|
|
1411
1736
|
label: "Workflow Catalog",
|
|
1412
1737
|
description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
|
|
1413
|
-
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or
|
|
1738
|
+
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
1414
1739
|
async execute(_id, params = {}) {
|
|
1415
|
-
const
|
|
1740
|
+
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
1741
|
+
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
1416
1742
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1417
|
-
}
|
|
1743
|
+
},
|
|
1744
|
+
renderCall(args, theme) {
|
|
1745
|
+
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
1746
|
+
return styledTextBlock(args.name === undefined ? title : `${title} ${theme.fg("accent", args.name)}`);
|
|
1747
|
+
},
|
|
1748
|
+
renderResult(result, options, theme) {
|
|
1749
|
+
return workflowCatalogBlock(formatWorkflowCatalog(catalogResultValue(result), options.expanded, theme), options.expanded);
|
|
1750
|
+
},
|
|
1418
1751
|
});
|
|
1419
1752
|
catalogRegistered = true;
|
|
1420
1753
|
};
|
|
@@ -1425,20 +1758,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1425
1758
|
if (missing)
|
|
1426
1759
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1427
1760
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
const
|
|
1761
|
+
const trustedProject = context?.projectTrusted ?? run.projectTrusted();
|
|
1762
|
+
const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
|
|
1763
|
+
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1764
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1431
1765
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1766
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1767
|
+
const inventory = modelInventory(rootModel, context?.modelRegistry);
|
|
1768
|
+
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1769
|
+
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1770
|
+
const currentAliases = (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: run.abortController.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
1437
1771
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1438
1772
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1439
|
-
const
|
|
1773
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1774
|
+
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1440
1775
|
await run.store.saveSnapshot(snapshot);
|
|
1441
|
-
|
|
1776
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1777
|
+
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);
|
|
1442
1778
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
1443
1779
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1444
1780
|
if (drift.length)
|
|
@@ -1462,28 +1798,35 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1462
1798
|
if (missing)
|
|
1463
1799
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1464
1800
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1465
|
-
const
|
|
1466
|
-
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1467
|
-
const
|
|
1801
|
+
const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
|
|
1802
|
+
const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
1803
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1468
1804
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1469
|
-
const
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1805
|
+
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
1806
|
+
const inventory = modelInventory(rootModel, context?.modelRegistry);
|
|
1807
|
+
const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
1808
|
+
const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
1809
|
+
const controller = new AbortController();
|
|
1810
|
+
if (context?.signal?.aborted)
|
|
1811
|
+
controller.abort();
|
|
1812
|
+
else {
|
|
1813
|
+
context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
|
|
1814
|
+
}
|
|
1815
|
+
run.abortController = controller;
|
|
1816
|
+
const currentAliases = context?.resolvedAliases ?? (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: controller.signal }, availableModels, knownModels, settingsPath)).aliases;
|
|
1474
1817
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1475
|
-
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1476
|
-
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1818
|
+
const blockedAliases = context?.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1819
|
+
const blockedAliasTargets = context?.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1477
1820
|
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1478
|
-
preflight(script, { models:
|
|
1479
|
-
const
|
|
1821
|
+
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);
|
|
1822
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
1823
|
+
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1480
1824
|
await run.store.saveSnapshot(snapshot);
|
|
1481
|
-
|
|
1825
|
+
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
1826
|
+
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);
|
|
1482
1827
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
1483
1828
|
if (drift.length)
|
|
1484
1829
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
1485
|
-
const controller = new AbortController();
|
|
1486
|
-
run.abortController = controller;
|
|
1487
1830
|
const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
|
|
1488
1831
|
run.executor.setRunContext(runContext);
|
|
1489
1832
|
let variables;
|
|
@@ -1562,7 +1905,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1562
1905
|
run.completion = completion;
|
|
1563
1906
|
void completion;
|
|
1564
1907
|
};
|
|
1565
|
-
const applyBudgetDecision = async (request, approved) => {
|
|
1908
|
+
const applyBudgetDecision = async (request, approved, context, signal) => {
|
|
1566
1909
|
const run = runs.get(request.runId);
|
|
1567
1910
|
if (!run)
|
|
1568
1911
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
|
|
@@ -1576,10 +1919,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1576
1919
|
next.budget = nextBudget;
|
|
1577
1920
|
else
|
|
1578
1921
|
delete next.budget; return next; });
|
|
1579
|
-
await coldResumeRun(run, false, {},
|
|
1922
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1580
1923
|
return { state: "running", approved: true };
|
|
1581
1924
|
};
|
|
1582
|
-
const resumeWorkflowRun = async (runId, rawPatch) => {
|
|
1925
|
+
const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
|
|
1583
1926
|
const run = runs.get(runId);
|
|
1584
1927
|
if (!run)
|
|
1585
1928
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${runId}`);
|
|
@@ -1610,11 +1953,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1610
1953
|
else
|
|
1611
1954
|
delete next.budget; return next; });
|
|
1612
1955
|
}
|
|
1613
|
-
await coldResumeRun(run, false, {},
|
|
1956
|
+
await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
|
|
1614
1957
|
return { state: "running" };
|
|
1615
1958
|
};
|
|
1616
1959
|
const retryReservations = new Set();
|
|
1617
|
-
const retryWorkflowRun = async (runId, context) => {
|
|
1960
|
+
const retryWorkflowRun = async (runId, context, signal) => {
|
|
1618
1961
|
if (typeof runId !== "string" || !runId.trim())
|
|
1619
1962
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
1620
1963
|
const host = object(context) ? context : {};
|
|
@@ -1674,19 +2017,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1674
2017
|
if (missing)
|
|
1675
2018
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
1676
2019
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1677
|
-
const
|
|
1678
|
-
resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
1679
|
-
const
|
|
2020
|
+
const resolution = resolveWorkflowSettings(cwd, trustedProject, settingsPath);
|
|
2021
|
+
const currentPolicy = resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
|
|
2022
|
+
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
1680
2023
|
const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
|
|
1681
2024
|
const modelRegistry = contextHostCapabilities(context).modelRegistry;
|
|
1682
|
-
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
|
|
1683
2025
|
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: "" };
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const
|
|
2026
|
+
const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
2027
|
+
const inventory = modelInventory(rootModel, modelRegistry);
|
|
2028
|
+
const knownModels = modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
|
|
2029
|
+
const availableModels = modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
|
|
2030
|
+
const resolvedAliases = await resolveLaunchAliases(registry, staticAliases, { cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: signal ?? new AbortController().signal }, availableModels, knownModels, settingsPath);
|
|
2031
|
+
const currentAliases = resolvedAliases.aliases;
|
|
1687
2032
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2033
|
+
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2034
|
+
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
1688
2035
|
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
1689
|
-
preflight(script, { models:
|
|
2036
|
+
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);
|
|
1690
2037
|
await sourceStore.validateNamedWorktrees();
|
|
1691
2038
|
for (const name of loaded.run.retry?.namedWorktrees ?? [])
|
|
1692
2039
|
await sourceStore.resolveNamedWorktree(name);
|
|
@@ -1696,7 +2043,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1696
2043
|
const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
|
|
1697
2044
|
const childRunId = randomUUID();
|
|
1698
2045
|
const childStore = new RunStore(cwd, sessionId, childRunId, home);
|
|
1699
|
-
const
|
|
2046
|
+
const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
|
|
2047
|
+
const childSnapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1700
2048
|
const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
|
|
1701
2049
|
const childInitialBudget = childBudget.snapshot();
|
|
1702
2050
|
const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
|
|
@@ -1705,13 +2053,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1705
2053
|
const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
|
|
1706
2054
|
const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
|
|
1707
2055
|
const abortController = new AbortController();
|
|
1708
|
-
const providerErrorRecovery = createProviderErrorRecovery(context,
|
|
2056
|
+
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
1709
2057
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1710
|
-
const childRun = { executor: new WorkflowAgentExecutor({ cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => active.has(tool) || tool === "workflow_catalog")), availableModels
|
|
2058
|
+
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 } : {}) };
|
|
1711
2059
|
runs.set(childRunId, childRun);
|
|
1712
2060
|
scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
|
|
1713
2061
|
await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
|
|
1714
|
-
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry
|
|
2062
|
+
await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
|
|
1715
2063
|
const completion = runs.get(childRunId)?.completion;
|
|
1716
2064
|
if (completion) {
|
|
1717
2065
|
childStarted = true;
|
|
@@ -1729,9 +2077,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1729
2077
|
label: "Workflow Retry",
|
|
1730
2078
|
description: "Retry a failed workflow run by replaying its completed structural operations",
|
|
1731
2079
|
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
1732
|
-
async execute(_id, params,
|
|
2080
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1733
2081
|
try {
|
|
1734
|
-
const result = await retryWorkflowRun(params.runId, ctx);
|
|
2082
|
+
const result = await retryWorkflowRun(params.runId, ctx, signal);
|
|
1735
2083
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1736
2084
|
}
|
|
1737
2085
|
catch (error) {
|
|
@@ -1744,9 +2092,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1744
2092
|
label: "Workflow Resume",
|
|
1745
2093
|
description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
1746
2094
|
parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
1747
|
-
async execute(_id, params) {
|
|
2095
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1748
2096
|
try {
|
|
1749
|
-
const result = await resumeWorkflowRun(params.runId, params.budget);
|
|
2097
|
+
const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
|
|
1750
2098
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1751
2099
|
}
|
|
1752
2100
|
catch (error) {
|
|
@@ -1759,7 +2107,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1759
2107
|
return;
|
|
1760
2108
|
sessionStarted = true;
|
|
1761
2109
|
registry.freeze();
|
|
1762
|
-
registerCatalog();
|
|
2110
|
+
registerCatalog(ctx.cwd, projectTrusted(ctx));
|
|
1763
2111
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1764
2112
|
try {
|
|
1765
2113
|
for (const runId of await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)) {
|
|
@@ -1795,7 +2143,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1795
2143
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
1796
2144
|
const abortController = new AbortController();
|
|
1797
2145
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
1798
|
-
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(), agentResourcePolicy: (
|
|
2146
|
+
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 } : {}) });
|
|
1799
2147
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
1800
2148
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
1801
2149
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -1832,7 +2180,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1832
2180
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1833
2181
|
if (!pi.getActiveTools().includes("workflow"))
|
|
1834
2182
|
return;
|
|
1835
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
2183
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
|
|
1836
2184
|
if (!roles.length)
|
|
1837
2185
|
return;
|
|
1838
2186
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
@@ -1848,32 +2196,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1848
2196
|
try {
|
|
1849
2197
|
const headless = object(ctx) && ctx.headless === true;
|
|
1850
2198
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
1851
|
-
const defaults = loadSettings(settingsPath);
|
|
1852
2199
|
if (!ctx.model)
|
|
1853
2200
|
throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
1854
|
-
const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
|
|
1855
2201
|
const budget = validateBudget(params.budget);
|
|
1856
2202
|
const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
1857
2203
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
1858
2204
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
1859
|
-
const
|
|
1860
|
-
knownModels.
|
|
1861
|
-
const availableModels =
|
|
2205
|
+
const inventory = modelInventory(rootModel, modelRegistry);
|
|
2206
|
+
const knownModels = inventory.knownModels;
|
|
2207
|
+
const availableModels = inventory.availableModels;
|
|
1862
2208
|
const rootTools = pi.getActiveTools().filter((name) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(name));
|
|
1863
2209
|
const trustedProject = projectTrusted(ctx);
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
1867
|
-
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
1868
|
-
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1869
|
-
const runId = randomUUID();
|
|
1870
|
-
const args = (params.args ?? null);
|
|
1871
|
-
encoded(args);
|
|
2210
|
+
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
2211
|
+
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
1872
2212
|
const runController = new AbortController();
|
|
1873
2213
|
if (signal?.aborted)
|
|
1874
2214
|
runController.abort();
|
|
1875
2215
|
else
|
|
1876
2216
|
signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
|
|
2217
|
+
const resolvedAliases = await resolveLaunchAliases(registry, launch.settings.modelAliases ?? {}, { cwd: launchCwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: runController.signal }, availableModels, knownModels, settingsPath);
|
|
2218
|
+
const modelAliases = resolvedAliases.aliases;
|
|
2219
|
+
const settings = Object.freeze({ ...launch.settings, ...(Object.keys(modelAliases).length ? { modelAliases } : {}) });
|
|
2220
|
+
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath }, registry);
|
|
2221
|
+
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
2222
|
+
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
2223
|
+
const runId = randomUUID();
|
|
2224
|
+
const args = (params.args ?? null);
|
|
2225
|
+
encoded(args);
|
|
1877
2226
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
1878
2227
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
1879
2228
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
@@ -1882,9 +2231,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1882
2231
|
await store.validateParentRun(parentRunId);
|
|
1883
2232
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
|
|
1884
2233
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
1885
|
-
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model,
|
|
2234
|
+
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
1886
2235
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
1887
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function", functionName } : {}), ...(
|
|
2236
|
+
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", 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 });
|
|
1888
2237
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
1889
2238
|
const initialBudget = budgetRuntime.snapshot();
|
|
1890
2239
|
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);
|
|
@@ -1893,7 +2242,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1893
2242
|
const providerPause = async () => { if (background)
|
|
1894
2243
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1895
2244
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
1896
|
-
const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases
|
|
2245
|
+
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);
|
|
1897
2246
|
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 } : {}) });
|
|
1898
2247
|
if (params.foreground && onUpdate)
|
|
1899
2248
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
@@ -2027,7 +2376,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2027
2376
|
return entries.filter((entry) => entry !== undefined);
|
|
2028
2377
|
};
|
|
2029
2378
|
let stores = await loadStores();
|
|
2030
|
-
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or
|
|
2379
|
+
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.";
|
|
2031
2380
|
const setWorkflowStatus = (text) => {
|
|
2032
2381
|
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
2033
2382
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
@@ -2044,7 +2393,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2044
2393
|
return keepContext ? "dashboard" : "done";
|
|
2045
2394
|
}
|
|
2046
2395
|
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
2047
|
-
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true);
|
|
2396
|
+
const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
|
|
2048
2397
|
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
2049
2398
|
return keepContext ? "dashboard" : "done";
|
|
2050
2399
|
}
|
|
@@ -2069,7 +2418,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2069
2418
|
if (action === "resume" && run) {
|
|
2070
2419
|
if (run.lifecycle.state === "budget_exhausted") {
|
|
2071
2420
|
const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
|
|
2072
|
-
const result = await resumeWorkflowRun(run.store.runId, patch);
|
|
2421
|
+
const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
|
|
2073
2422
|
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");
|
|
2074
2423
|
}
|
|
2075
2424
|
else {
|
|
@@ -2077,7 +2426,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2077
2426
|
await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
|
|
2078
2427
|
else {
|
|
2079
2428
|
if (run.lifecycle.state === "paused")
|
|
2080
|
-
await refreshPausedRunAliases(run, ctx);
|
|
2429
|
+
await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
2081
2430
|
await run.lifecycle.resume();
|
|
2082
2431
|
}
|
|
2083
2432
|
ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
|
|
@@ -2088,7 +2437,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2088
2437
|
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
2089
2438
|
if (input === undefined)
|
|
2090
2439
|
return keepContext ? "dashboard" : "done";
|
|
2091
|
-
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
2440
|
+
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
|
|
2092
2441
|
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");
|
|
2093
2442
|
return keepContext ? "dashboard" : "done";
|
|
2094
2443
|
}
|
|
@@ -2123,6 +2472,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2123
2472
|
};
|
|
2124
2473
|
const manageAliases = async () => {
|
|
2125
2474
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2475
|
+
let aliasSettingsPath = settingsPath;
|
|
2476
|
+
const trustedProject = projectTrusted(ctx);
|
|
2126
2477
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
2127
2478
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
2128
2479
|
const selectTarget = async (aliases) => {
|
|
@@ -2136,22 +2487,24 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2136
2487
|
};
|
|
2137
2488
|
const save = (aliases) => {
|
|
2138
2489
|
try {
|
|
2139
|
-
saveModelAliases(
|
|
2140
|
-
ctx.ui.notify(`Saved model aliases to ${
|
|
2490
|
+
saveModelAliases(aliasSettingsPath, aliases);
|
|
2491
|
+
ctx.ui.notify(`Saved model aliases to ${aliasSettingsPath}.`, "info");
|
|
2141
2492
|
return true;
|
|
2142
2493
|
}
|
|
2143
2494
|
catch (error) {
|
|
2144
|
-
ctx.ui.notify(`${
|
|
2495
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2145
2496
|
return false;
|
|
2146
2497
|
}
|
|
2147
2498
|
};
|
|
2148
2499
|
for (;;) {
|
|
2149
2500
|
let aliases;
|
|
2150
2501
|
try {
|
|
2151
|
-
|
|
2502
|
+
const resolution = resolveWorkflowSettings(ctx.cwd, trustedProject, settingsPath);
|
|
2503
|
+
aliases = resolution.effective.modelAliases ?? {};
|
|
2504
|
+
aliasSettingsPath = resolution.sources.modelAliases;
|
|
2152
2505
|
}
|
|
2153
2506
|
catch (error) {
|
|
2154
|
-
ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2507
|
+
ctx.ui.notify(`${trustedProject ? workflowProjectSettingsPath(ctx.cwd) : settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2155
2508
|
return;
|
|
2156
2509
|
}
|
|
2157
2510
|
const names = Object.keys(aliases).sort();
|
|
@@ -2173,13 +2526,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2173
2526
|
continue;
|
|
2174
2527
|
const next = { ...aliases, [name]: target };
|
|
2175
2528
|
try {
|
|
2176
|
-
validateModelAliases(next,
|
|
2529
|
+
validateModelAliases(next, aliasSettingsPath);
|
|
2177
2530
|
}
|
|
2178
2531
|
catch (error) {
|
|
2179
|
-
ctx.ui.notify(`${
|
|
2532
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2180
2533
|
continue;
|
|
2181
2534
|
}
|
|
2182
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2535
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
2183
2536
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
2184
2537
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
2185
2538
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
|
|
@@ -2195,13 +2548,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2195
2548
|
continue;
|
|
2196
2549
|
const next = { ...aliases, [edit[1]]: target };
|
|
2197
2550
|
try {
|
|
2198
|
-
validateModelAliases(next,
|
|
2551
|
+
validateModelAliases(next, aliasSettingsPath);
|
|
2199
2552
|
}
|
|
2200
2553
|
catch (error) {
|
|
2201
|
-
ctx.ui.notify(`${
|
|
2554
|
+
ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2202
2555
|
continue;
|
|
2203
2556
|
}
|
|
2204
|
-
const parsed = resolveModelReference(target, next, new Set(available()),
|
|
2557
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
2205
2558
|
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
2206
2559
|
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
2207
2560
|
if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
|
|
@@ -2240,7 +2593,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2240
2593
|
const labels = navigatorRunLabels(sorted);
|
|
2241
2594
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
2242
2595
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
2243
|
-
const
|
|
2596
|
+
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
2597
|
+
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
2244
2598
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
2245
2599
|
if (!runChoice || runChoice === "Close")
|
|
2246
2600
|
return;
|
|
@@ -2273,6 +2627,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2273
2627
|
stores = await loadStores();
|
|
2274
2628
|
continue;
|
|
2275
2629
|
}
|
|
2630
|
+
if (runChoice === "Delete all failed") {
|
|
2631
|
+
if (!await ctx.ui.confirm("Delete failed runs?", "Delete all failed workflow runs and their artifacts? This cannot be undone."))
|
|
2632
|
+
continue;
|
|
2633
|
+
for (const entry of sorted) {
|
|
2634
|
+
if (entry.loaded.run.state === "failed") {
|
|
2635
|
+
await entry.store.delete(true);
|
|
2636
|
+
runs.delete(entry.store.runId);
|
|
2637
|
+
terminalRunStates.delete(entry.store.runId);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
ctx.ui.notify("Deleted all failed workflow runs.", "info");
|
|
2641
|
+
stores = await loadStores();
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2276
2644
|
const runIndex = labels.indexOf(runChoice);
|
|
2277
2645
|
if (runIndex < 0)
|
|
2278
2646
|
return;
|
|
@@ -2336,20 +2704,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2336
2704
|
addCopy("Copy run path", store.directory, "run path");
|
|
2337
2705
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
2338
2706
|
}
|
|
2339
|
-
return { dashboard:
|
|
2707
|
+
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 };
|
|
2340
2708
|
};
|
|
2341
2709
|
const selectAgent = async (dashboard) => {
|
|
2342
2710
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
2343
|
-
const title = (agent) =>
|
|
2344
|
-
const parents = [];
|
|
2345
|
-
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
2346
|
-
const parent = byId.get(parentId);
|
|
2347
|
-
if (!parent)
|
|
2348
|
-
break;
|
|
2349
|
-
parents.unshift(parent.label ?? parent.name);
|
|
2350
|
-
}
|
|
2351
|
-
return [...((agent.structuralPath ?? []).length ? [(agent.structuralPath ?? []).join(" > ")] : []), ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : []), ...parents, agent.label ?? agent.name].join(" > ");
|
|
2352
|
-
};
|
|
2711
|
+
const title = (agent) => agentBreadcrumb(agent, byId, true);
|
|
2353
2712
|
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
2354
2713
|
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
2355
2714
|
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
@@ -2414,8 +2773,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2414
2773
|
let disposed = false;
|
|
2415
2774
|
let stopRequested = false;
|
|
2416
2775
|
let stopStatus;
|
|
2776
|
+
const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
|
|
2777
|
+
let selectedPhaseId = initialSelection.phaseId;
|
|
2778
|
+
let selectedAgentId = initialSelection.agentId;
|
|
2417
2779
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
2418
|
-
const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
2780
|
+
const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", tab: "tab", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
2419
2781
|
const keyLabel = (binding, fallback) => {
|
|
2420
2782
|
const keys = keybindingKeys(keybindings, binding);
|
|
2421
2783
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
@@ -2429,12 +2791,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2429
2791
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
2430
2792
|
};
|
|
2431
2793
|
const updateDashboard = async (selectedOption) => {
|
|
2794
|
+
const phaseId = selectedPhaseId;
|
|
2795
|
+
const agentId = selectedAgentId;
|
|
2432
2796
|
const next = await loadDashboard();
|
|
2433
2797
|
if (disposed)
|
|
2434
2798
|
return;
|
|
2435
2799
|
view = next;
|
|
2436
2800
|
options = [...view.actions.keys(), "Close"];
|
|
2437
2801
|
selectedIndex = Math.max(0, options.indexOf(selectedOption ?? ""));
|
|
2802
|
+
const preserved = preserveWorkflowPhaseSelection(view.phaseModel, { phaseId, agentId });
|
|
2803
|
+
selectedPhaseId = preserved.phaseId;
|
|
2804
|
+
selectedAgentId = preserved.agentId;
|
|
2438
2805
|
tui.requestRender();
|
|
2439
2806
|
};
|
|
2440
2807
|
const requestStop = () => {
|
|
@@ -2470,11 +2837,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2470
2837
|
timer.unref();
|
|
2471
2838
|
return borderWorkflowOverlay({
|
|
2472
2839
|
render(width) {
|
|
2473
|
-
const
|
|
2474
|
-
const
|
|
2840
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
2841
|
+
const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { phaseId: selectedPhaseId, agentId: selectedAgentId }, styles);
|
|
2842
|
+
const dashboardLines = stopStatus ? [styles.error(stopStatus), ...phaseLines] : phaseLines;
|
|
2475
2843
|
const actionLines = options.map((option, index) => truncateToVisualLines(`${index === selectedIndex ? "→ " : " "}${option}`, Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "");
|
|
2476
2844
|
const layout = dashboardLayout();
|
|
2477
|
-
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}
|
|
2845
|
+
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] ?? "";
|
|
2478
2846
|
const compact = [...dashboardLines, "", ...actionLines, "", hint];
|
|
2479
2847
|
if (compact.length <= layout.rows) {
|
|
2480
2848
|
dashboardOffset = 0;
|
|
@@ -2494,7 +2862,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2494
2862
|
handleInput(data) {
|
|
2495
2863
|
if (stopRequested)
|
|
2496
2864
|
return;
|
|
2497
|
-
|
|
2865
|
+
const currentPhase = () => view.phaseModel.phases.find((phase) => phase.id === selectedPhaseId);
|
|
2866
|
+
const left = keybindings.matches(data, "tui.editor.cursorLeft");
|
|
2867
|
+
const right = keybindings.matches(data, "tui.editor.cursorRight");
|
|
2868
|
+
if (left || right) {
|
|
2869
|
+
const phases = view.phaseModel.phases;
|
|
2870
|
+
if (phases.length) {
|
|
2871
|
+
const currentIndex = Math.max(0, phases.findIndex((phase) => phase.id === selectedPhaseId));
|
|
2872
|
+
const delta = left ? -1 : 1;
|
|
2873
|
+
const nextPhase = phases[(currentIndex + delta + phases.length) % phases.length];
|
|
2874
|
+
selectedPhaseId = nextPhase?.id;
|
|
2875
|
+
selectedAgentId = nextPhase?.agents[0]?.id;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
else if (keybindings.matches(data, "tui.input.tab")) {
|
|
2879
|
+
const agents = currentPhase()?.agents ?? [];
|
|
2880
|
+
if (agents.length) {
|
|
2881
|
+
const currentIndex = Math.max(0, agents.findIndex((agent) => agent.id === selectedAgentId));
|
|
2882
|
+
selectedAgentId = agents[(currentIndex + 1) % agents.length]?.id;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
else if (keybindings.matches(data, "tui.select.up"))
|
|
2498
2886
|
selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
2499
2887
|
else if (keybindings.matches(data, "tui.select.down"))
|
|
2500
2888
|
selectedIndex = (selectedIndex + 1) % options.length;
|