@signalridge/pi-workflows 1.3.2 → 1.5.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/CHANGELOG.md +140 -0
- package/README.md +93 -22
- package/package.json +5 -4
- package/skills/workflow-authoring/SKILL.md +171 -18
- package/skills/workflow-authoring/examples/named-task-graph.js +34 -0
- package/skills/workflow-authoring/references/capabilities.md +4 -3
- package/skills/workflow-authoring/references/capability-details.md +28 -8
- package/skills/workflow-authoring/references/debugging.md +2 -2
- package/skills/workflow-authoring/references/focused-recipes.md +1 -0
- package/skills/workflow-authoring/references/lifecycle.md +3 -3
- package/skills/workflow-authoring/references/pattern-selection.md +1 -0
- package/skills/workflow-authoring/references/registry-ownership.md +6 -4
- package/skills/workflow-authoring/references/review.md +12 -7
- package/skills/workflow-authoring/references/runtime.md +6 -5
- package/skills/workflow-authoring/references/versions.md +1 -1
- package/skills/workflow-patterns/SKILL.md +72 -33
- package/skills/workflow-review/SKILL.md +70 -0
- package/src/builtins.ts +23 -23
- package/src/capabilities.ts +39 -7
- package/src/engine.ts +582 -142
- package/src/index.ts +413 -154
- package/src/journal.ts +447 -169
- package/src/orchestrator.ts +478 -0
- package/src/rpc-client.ts +37 -48
- package/src/runtime.ts +508 -167
- package/src/ui.ts +29 -4
package/src/index.ts
CHANGED
|
@@ -8,7 +8,12 @@ import { BUILTIN_WORKFLOWS, validateBuiltinArgs } from "./builtins.js";
|
|
|
8
8
|
import { type CommandResult, resolveCodeReviewScope } from "./code-review-scope.js";
|
|
9
9
|
import { type ScriptStartOptions, type ScriptStartResult, WorkflowEngine, WorkflowWaitAbortedError } from "./engine.js";
|
|
10
10
|
import { JOURNAL_ENTRY_TYPE, type SessionEntryLike } from "./journal.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createManagedSpawnClient,
|
|
13
|
+
type ManagedProtocolCheck,
|
|
14
|
+
PROTOCOL_DIAGNOSTIC,
|
|
15
|
+
queryChildSessionContextImmediate,
|
|
16
|
+
} from "./rpc-client.js";
|
|
12
17
|
import { listSavedWorkflows, loadSavedWorkflow, saveWorkflow } from "./saved-workflows.js";
|
|
13
18
|
import { liveWidgetLines, showWorkflowNavigator } from "./ui.js";
|
|
14
19
|
import { loadWorkflowSettings, saveWorkflowSettings } from "./workflow-settings.js";
|
|
@@ -118,18 +123,170 @@ function formatStart(result: ScriptStartResult): string {
|
|
|
118
123
|
return lines.join("\n");
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
/** Build the bounded planner → graph → synthesis script used by `/workflows run`. */
|
|
127
|
+
export function buildAdHocWorkflowScript(prompt: string): string {
|
|
128
|
+
return `export const meta = { name: "ad-hoc", description: "Plan, execute, and synthesize a bounded multi-agent task graph" };
|
|
129
|
+
const originalPrompt = ${JSON.stringify(prompt)};
|
|
130
|
+
const plan = await agent("Design a bounded execution plan for this user task. Return 1-8 independent or dependency-linked tasks. Use unique short ids, concise descriptions, exact worker prompts, and only dependencies that appear in the task list. Prefer independent tasks when possible. User task:\\n\\n" + originalPrompt, {
|
|
131
|
+
label: "planner",
|
|
132
|
+
tier: "medium",
|
|
133
|
+
schema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
tasks: {
|
|
137
|
+
type: "array",
|
|
138
|
+
minItems: 1,
|
|
139
|
+
maxItems: 8,
|
|
140
|
+
items: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
id: { type: "string", minLength: 1, maxLength: 64 },
|
|
144
|
+
description: { type: "string", minLength: 1, maxLength: 500 },
|
|
145
|
+
prompt: { type: "string", minLength: 1, maxLength: 8000 },
|
|
146
|
+
dependsOn: { type: "array", maxItems: 8, items: { type: "string", minLength: 1, maxLength: 64 } },
|
|
147
|
+
},
|
|
148
|
+
required: ["id", "description", "prompt"],
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ["tasks"],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const graph = await orchestrate(plan.tasks.map((task) => ({
|
|
158
|
+
id: task.id,
|
|
159
|
+
description: task.description,
|
|
160
|
+
dependsOn: task.dependsOn || [],
|
|
161
|
+
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id, tier: "low" }),
|
|
162
|
+
})), { onError: "continue" });
|
|
163
|
+
const SYNTHESIS_CONTEXT_LIMIT = 78000;
|
|
164
|
+
function renderContextValue(value) {
|
|
165
|
+
const rendered = JSON.stringify(value);
|
|
166
|
+
return rendered === undefined ? String(value) : rendered;
|
|
167
|
+
}
|
|
168
|
+
function boundContextText(value, maxEncodedChars) {
|
|
169
|
+
const text = String(value);
|
|
170
|
+
const encodedLength = (candidate) => JSON.stringify(candidate).length - 2;
|
|
171
|
+
if (encodedLength(text) <= maxEncodedChars) return { text, truncated: false, marker: null };
|
|
172
|
+
let low = 0;
|
|
173
|
+
let high = text.length;
|
|
174
|
+
while (low < high) {
|
|
175
|
+
const middle = Math.ceil((low + high) / 2);
|
|
176
|
+
if (encodedLength(text.slice(0, middle)) <= maxEncodedChars) low = middle;
|
|
177
|
+
else high = middle - 1;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
text: text.slice(0, low),
|
|
181
|
+
truncated: true,
|
|
182
|
+
marker: "[TRUNCATED " + (text.length - low) + " SOURCE CHARACTERS]",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const planMetadata = {
|
|
186
|
+
taskCount: plan.tasks.length,
|
|
187
|
+
tasks: plan.tasks.map((task) => {
|
|
188
|
+
const description = boundContextText(task.description, 500);
|
|
189
|
+
return {
|
|
190
|
+
id: task.id,
|
|
191
|
+
description: description.text,
|
|
192
|
+
descriptionTruncated: description.truncated,
|
|
193
|
+
descriptionTruncationMarker: description.marker,
|
|
194
|
+
dependsOn: task.dependsOn || [],
|
|
195
|
+
};
|
|
196
|
+
}),
|
|
197
|
+
};
|
|
198
|
+
function makeSynthesisContext(valuePreviewBudget, originalPromptBudget) {
|
|
199
|
+
const boundedOriginalPrompt = boundContextText(originalPrompt, originalPromptBudget);
|
|
200
|
+
const tasks = Object.keys(graph.tasks).map((id) => {
|
|
201
|
+
const task = graph.tasks[id];
|
|
202
|
+
const error = task.error == null ? null : boundContextText(task.error, 1000);
|
|
203
|
+
const value = boundContextText(renderContextValue(task.value), valuePreviewBudget);
|
|
204
|
+
return {
|
|
205
|
+
id,
|
|
206
|
+
status: task.status,
|
|
207
|
+
attempts: task.attempts,
|
|
208
|
+
error: error == null ? null : error.text,
|
|
209
|
+
errorTruncated: error == null ? false : error.truncated,
|
|
210
|
+
errorTruncationMarker: error == null ? null : error.marker,
|
|
211
|
+
valuePreview: value.text,
|
|
212
|
+
valueTruncated: value.truncated,
|
|
213
|
+
valueTruncationMarker: value.marker,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
originalPrompt: boundedOriginalPrompt.text,
|
|
218
|
+
originalPromptTruncated: boundedOriginalPrompt.truncated,
|
|
219
|
+
originalPromptTruncationMarker: boundedOriginalPrompt.marker,
|
|
220
|
+
plan: planMetadata,
|
|
221
|
+
tasks,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
let valuePreviewBudget = 6000;
|
|
225
|
+
let originalPromptBudget = 16000;
|
|
226
|
+
let synthesisContext = makeSynthesisContext(valuePreviewBudget, originalPromptBudget);
|
|
227
|
+
let synthesisJson = JSON.stringify(synthesisContext);
|
|
228
|
+
while (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT && (valuePreviewBudget > 128 || originalPromptBudget > 256)) {
|
|
229
|
+
valuePreviewBudget = Math.max(128, Math.floor(valuePreviewBudget * 0.75));
|
|
230
|
+
originalPromptBudget = Math.max(256, Math.floor(originalPromptBudget * 0.75));
|
|
231
|
+
synthesisContext = makeSynthesisContext(valuePreviewBudget, originalPromptBudget);
|
|
232
|
+
synthesisJson = JSON.stringify(synthesisContext);
|
|
233
|
+
}
|
|
234
|
+
if (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT) throw new Error("Unable to fit the synthesis context within its managed prompt budget");
|
|
235
|
+
return await agent("Synthesize the worker results into a direct answer to the original task. Mention incomplete or failed coverage explicitly.\\n\\nOriginal task, execution plan, and graph ledger:\\n" + synthesisJson, { label: "synthesizer", tier: "medium" });`;
|
|
236
|
+
}
|
|
237
|
+
|
|
121
238
|
export default function piWorkflows(pi: ExtensionAPI): void {
|
|
122
239
|
let engine: WorkflowEngine | undefined;
|
|
123
240
|
let protocolError: string | undefined;
|
|
124
|
-
let protocolCheck: Promise<
|
|
241
|
+
let protocolCheck: Promise<ManagedProtocolCheck> | undefined;
|
|
242
|
+
let protocolProbe: (() => Promise<ManagedProtocolCheck>) | undefined;
|
|
243
|
+
let protocolInFlight: Promise<ManagedProtocolCheck> | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* Track one in-flight probe so concurrent starts share it, and release it the
|
|
246
|
+
* moment it settles.
|
|
247
|
+
*
|
|
248
|
+
* Releasing matters: the catalogue is read live on the peer's side, so the
|
|
249
|
+
* next start or resume must issue a *new* ping. Holding a settled promise
|
|
250
|
+
* here would pin every later run to the routing policy as it stood at session
|
|
251
|
+
* start, and a tier defined since then would be rejected pre-dispatch as
|
|
252
|
+
* unknown.
|
|
253
|
+
*/
|
|
254
|
+
const trackProbe = (check: Promise<ManagedProtocolCheck>): Promise<ManagedProtocolCheck> => {
|
|
255
|
+
protocolInFlight = check;
|
|
256
|
+
const release = () => {
|
|
257
|
+
if (protocolInFlight === check) protocolInFlight = undefined;
|
|
258
|
+
};
|
|
259
|
+
void check.then(release, release);
|
|
260
|
+
return check;
|
|
261
|
+
};
|
|
262
|
+
const awaitProtocol = async (): Promise<ManagedProtocolCheck> => {
|
|
263
|
+
let check = protocolInFlight;
|
|
264
|
+
if (!check) {
|
|
265
|
+
if (protocolProbe) protocolError = undefined;
|
|
266
|
+
check = protocolProbe?.() ?? protocolCheck;
|
|
267
|
+
if (!check) throw new Error(`${PROTOCOL_DIAGNOSTIC} Diagnostic: no active protocol probe`);
|
|
268
|
+
trackProbe(check);
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const result = await check;
|
|
272
|
+
protocolError = undefined;
|
|
273
|
+
return result;
|
|
274
|
+
} catch (error: unknown) {
|
|
275
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
276
|
+
throw new Error(protocolError ?? `${PROTOCOL_DIAGNOSTIC} Diagnostic: ${detail}`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
125
279
|
let active = false;
|
|
126
|
-
let
|
|
280
|
+
let surfaceRegistered = false;
|
|
281
|
+
const registeredWorkflowCommands = new Set<string>();
|
|
127
282
|
let branchQuiesce: Promise<{ settled: boolean; pending: string[]; diagnostic?: string }> | undefined;
|
|
128
283
|
let lifecycleGeneration = 0;
|
|
129
284
|
let protocolAbortController: AbortController | undefined;
|
|
130
285
|
let sessionCwd = process.cwd();
|
|
131
286
|
let workflowSettings = loadWorkflowSettings(sessionCwd);
|
|
132
287
|
let effortLevel: "off" | "high" | "ultra" = workflowSettings.effort;
|
|
288
|
+
let triggerKeyword: string | undefined = workflowSettings.keywordTriggerWord;
|
|
289
|
+
let keywordTriggerEnabled = workflowSettings.keywordTriggerEnabled;
|
|
133
290
|
/** Per-session delivery endpoint for background-run results (A10 fail-closed). */
|
|
134
291
|
let deliverResult: ((text: string, details?: unknown) => void) | undefined;
|
|
135
292
|
/** Pending deliveries for runs that settled before a session endpoint was bound (A10). */
|
|
@@ -141,10 +298,42 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
141
298
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
142
299
|
|
|
143
300
|
const currentEngine = (): WorkflowEngine => {
|
|
144
|
-
if (!
|
|
301
|
+
if (!active || !engine || engine.isDisposed())
|
|
302
|
+
throw new Error("pi-workflows is not active in this session context");
|
|
145
303
|
return engine;
|
|
146
304
|
};
|
|
147
305
|
|
|
306
|
+
const clearWidget = (): void => {
|
|
307
|
+
const tui = (pi as unknown as { ui?: { setWidget?: (key: string, content: string[] | undefined) => void } }).ui;
|
|
308
|
+
tui?.setWidget?.("pi-workflows", undefined);
|
|
309
|
+
};
|
|
310
|
+
const refreshWidget = (): void => {
|
|
311
|
+
const tui = (pi as unknown as { ui?: { setWidget?: (key: string, content: string[] | undefined) => void } }).ui;
|
|
312
|
+
if (!tui?.setWidget) return;
|
|
313
|
+
const workflowEngine = engine;
|
|
314
|
+
if (!active || !workflowEngine || workflowEngine.isDisposed()) {
|
|
315
|
+
tui.setWidget("pi-workflows", undefined);
|
|
316
|
+
if (widgetTimer) {
|
|
317
|
+
clearInterval(widgetTimer);
|
|
318
|
+
widgetTimer = undefined;
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const runs = workflowEngine
|
|
323
|
+
.list()
|
|
324
|
+
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
325
|
+
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
326
|
+
const lines = liveWidgetLines(runs, workflowSettings.progressMode, workflowSettings.maxAgentsShown);
|
|
327
|
+
tui.setWidget("pi-workflows", lines.length > 0 ? lines : undefined);
|
|
328
|
+
if (lines.length > 0 && !widgetTimer) {
|
|
329
|
+
widgetTimer = setInterval(refreshWidget, 2_000);
|
|
330
|
+
} else if (lines.length === 0 && widgetTimer) {
|
|
331
|
+
clearInterval(widgetTimer);
|
|
332
|
+
widgetTimer = undefined;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const notifyRunChanged = (): void => refreshWidget();
|
|
336
|
+
|
|
148
337
|
const branchEntries = (): SessionEntryLike[] => {
|
|
149
338
|
const ctx = (pi as unknown as { currentSessionManager?: { getBranch?: () => unknown } }).currentSessionManager;
|
|
150
339
|
try {
|
|
@@ -156,16 +345,38 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
156
345
|
|
|
157
346
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
158
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Resolve a nested `workflow(name)` reference, saying where the script came
|
|
350
|
+
* from.
|
|
351
|
+
*
|
|
352
|
+
* Shipped-ness has to be reported per script rather than inherited from the
|
|
353
|
+
* frame that called it: a user script may call a built-in by name, and a
|
|
354
|
+
* built-in's tier names are preferences against whatever catalogue the host
|
|
355
|
+
* defines, while a user script's are assertions about a catalogue the user
|
|
356
|
+
* owns. Inheriting the caller's flag would apply the wrong rule in both
|
|
357
|
+
* directions.
|
|
358
|
+
*/
|
|
359
|
+
const resolveNestedWorkflow = (name: string): { script: string; shippedScript?: boolean } | undefined => {
|
|
360
|
+
// A saved file shadows a built-in of the same name; it is the user's own
|
|
361
|
+
// script and keeps the strict tier check.
|
|
362
|
+
const saved = loadSavedWorkflow(name, sessionCwd);
|
|
363
|
+
if (saved !== undefined) return { script: saved };
|
|
364
|
+
const builtin = BUILTIN_WORKFLOWS[name]?.script;
|
|
365
|
+
return builtin === undefined ? undefined : { script: builtin, shippedScript: true };
|
|
366
|
+
};
|
|
367
|
+
|
|
159
368
|
const resolveScript = (
|
|
160
369
|
script: string | undefined,
|
|
161
370
|
name: string | undefined,
|
|
162
371
|
args: unknown,
|
|
163
|
-
): { script: string; source: string; toolset?: string } => {
|
|
372
|
+
): { script: string; source: string; toolset?: string; shippedScript?: boolean } => {
|
|
164
373
|
if (script !== undefined && name !== undefined) {
|
|
165
374
|
throw new Error("Provide either `script` or `name`, not both.");
|
|
166
375
|
}
|
|
167
376
|
if (script !== undefined) return { script, source: "inline" };
|
|
168
377
|
if (name !== undefined) {
|
|
378
|
+
// A saved workflow is the user's own file even when it shadows a built-in
|
|
379
|
+
// name, so it keeps the strict tier check.
|
|
169
380
|
const saved = loadSavedWorkflow(name, sessionCwd);
|
|
170
381
|
if (saved) return { script: saved, source: `saved:${name}` };
|
|
171
382
|
const builtin = BUILTIN_WORKFLOWS[name];
|
|
@@ -173,7 +384,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
173
384
|
// Validate named invocations before starting a potentially expensive fleet.
|
|
174
385
|
// Slash commands provide the same shape through their descriptor primaryArg.
|
|
175
386
|
validateBuiltinArgs(name, args);
|
|
176
|
-
return {
|
|
387
|
+
return {
|
|
388
|
+
script: builtin.script,
|
|
389
|
+
source: `builtin:${name}`,
|
|
390
|
+
toolset: builtin.toolset,
|
|
391
|
+
shippedScript: true,
|
|
392
|
+
};
|
|
177
393
|
}
|
|
178
394
|
throw new Error(`Unknown workflow name "${name}". It is neither a saved workflow nor a built-in.`);
|
|
179
395
|
}
|
|
@@ -184,17 +400,23 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
184
400
|
if (active) return;
|
|
185
401
|
active = true;
|
|
186
402
|
const workflowEngine = currentEngine();
|
|
403
|
+
workflowEngine.onRunSettled = notifyRunChanged;
|
|
404
|
+
// Command/tool registrations persist across session replacement, but the
|
|
405
|
+
// widget belongs to the newly restored engine and must be refreshed too.
|
|
406
|
+
refreshWidget();
|
|
407
|
+
if (surfaceRegistered) return;
|
|
408
|
+
surfaceRegistered = true;
|
|
187
409
|
|
|
188
410
|
pi.registerTool(
|
|
189
411
|
defineTool({
|
|
190
412
|
name: "workflow",
|
|
191
413
|
label: "Workflow",
|
|
192
414
|
description:
|
|
193
|
-
"Run a JavaScript workflow script through the pi-subagents managed spawning protocol. The script declares `export const meta = { name, description, phases }` first and uses the runtime globals agent(), parallel(), pipeline(), workflow(), verify(), judgePanel(), loopUntilDry(), completenessCheck(), retry(), gate(), checkpoint(), phase(), log(), args, cwd, process, and budget. Determinism is enforced: Date.now()/Math.random()/new Date() are unavailable.",
|
|
415
|
+
"Run a JavaScript workflow script through the pi-subagents managed spawning protocol. The script declares `export const meta = { name, description, phases }` first and uses the runtime globals agent(), parallel(), pipeline(), orchestrate(), workflow(), verify(), judgePanel(), loopUntilDry(), completenessCheck(), retry(), gate(), checkpoint(), phase(), log(), args, cwd, process, and budget. Determinism is enforced: Date.now()/Math.random()/new Date() are unavailable.",
|
|
194
416
|
promptSnippet: "Run a JavaScript workflow script",
|
|
195
417
|
promptGuidelines: [
|
|
196
418
|
"Provide either a full `script` (raw JavaScript with the meta contract first) or a `name` of a saved/built-in workflow.",
|
|
197
|
-
"Use parallel(() => agent(...), ...) for
|
|
419
|
+
"Use orchestrate([{ id, dependsOn, run }]) for named dependency graphs; use parallel(() => agent(...), ...) for a simple fan-out and pipeline(items, ...stages) for sequential per-item stages.",
|
|
198
420
|
"Use background: true for long runs; retrieve results with workflow_control.",
|
|
199
421
|
"Use resumeFromRunId with an edited script to replay the unchanged prefix from cache and re-run the rest live.",
|
|
200
422
|
"Do not include model, thinking, concurrency, retry, timeout, or turn-limit settings beyond the declared options.",
|
|
@@ -219,62 +441,67 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
219
441
|
},
|
|
220
442
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
221
443
|
try {
|
|
222
|
-
|
|
444
|
+
const workflowEngine = currentEngine();
|
|
445
|
+
const resolved =
|
|
446
|
+
params.resumeFromRunId && params.script === undefined && params.name === undefined
|
|
447
|
+
? (() => {
|
|
448
|
+
const prior = workflowEngine.getRun(params.resumeFromRunId);
|
|
449
|
+
if (!prior) throw new Error(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
450
|
+
// Resume reads the frozen flag off the restored run, so
|
|
451
|
+
// this only has to satisfy the shared shape.
|
|
452
|
+
return { script: prior.script, source: "resume", toolset: prior.toolset, shippedScript: undefined };
|
|
453
|
+
})()
|
|
454
|
+
: resolveScript(params.script, params.name, params.args);
|
|
455
|
+
const { script, source, toolset, shippedScript } = resolved;
|
|
456
|
+
const options: ScriptStartOptions = {
|
|
457
|
+
args: params.args,
|
|
458
|
+
background: params.background ?? true,
|
|
459
|
+
maxAgents: params.maxAgents,
|
|
460
|
+
concurrency: params.concurrency,
|
|
461
|
+
agentRetries: params.agentRetries,
|
|
462
|
+
tokenBudget: params.tokenBudget,
|
|
463
|
+
agentTimeoutMs: params.agentTimeoutMs,
|
|
464
|
+
toolset,
|
|
465
|
+
...(shippedScript === undefined ? {} : { shippedScript }),
|
|
466
|
+
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
467
|
+
signal,
|
|
468
|
+
mainModel: ctx?.model?.id,
|
|
469
|
+
// Foreground checkpoint confirmation only when the run is not
|
|
470
|
+
// background — background runs are headless by contract.
|
|
471
|
+
confirm:
|
|
472
|
+
params.background === false && ctx?.hasUI && ctx.mode === "tui"
|
|
473
|
+
? async (promptText, checkpointOptions) => {
|
|
474
|
+
if (checkpointOptions.kind === "input")
|
|
475
|
+
return ctx.ui.input(promptText, String(checkpointOptions.default ?? ""));
|
|
476
|
+
if (checkpointOptions.kind === "select")
|
|
477
|
+
return ctx.ui.select(promptText, checkpointOptions.choices ?? []);
|
|
478
|
+
return ctx.ui.confirm(promptText, promptText);
|
|
479
|
+
}
|
|
480
|
+
: undefined,
|
|
481
|
+
loadSavedWorkflow: resolveNestedWorkflow,
|
|
482
|
+
};
|
|
483
|
+
if (params.resumeFromRunId) {
|
|
484
|
+
const replacement = params.script !== undefined || params.name !== undefined ? script : undefined;
|
|
485
|
+
const resumed = await workflowEngine.resume(
|
|
486
|
+
params.resumeFromRunId,
|
|
487
|
+
branchEntries(),
|
|
488
|
+
options,
|
|
489
|
+
replacement,
|
|
490
|
+
);
|
|
491
|
+
if (!resumed) return textResult(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
492
|
+
if (resumed.background) void deliverBackgroundResult(workflowEngine, resumed.runId);
|
|
493
|
+
return textResult(formatStart(resumed), resumed);
|
|
494
|
+
}
|
|
495
|
+
const result = await workflowEngine.start(script, options);
|
|
496
|
+
if (result.background) {
|
|
497
|
+
// The engine resolves the script's meta name for the delivery header.
|
|
498
|
+
void source;
|
|
499
|
+
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
500
|
+
}
|
|
501
|
+
return textResult(formatStart(result), result);
|
|
223
502
|
} catch (error: unknown) {
|
|
224
|
-
return textResult(
|
|
225
|
-
protocolError ??
|
|
226
|
-
`${PROTOCOL_DIAGNOSTIC} Diagnostic: ${error instanceof Error ? error.message : String(error)}`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
const resolved =
|
|
230
|
-
params.resumeFromRunId && params.script === undefined && params.name === undefined
|
|
231
|
-
? (() => {
|
|
232
|
-
const prior = workflowEngine.getRun(params.resumeFromRunId);
|
|
233
|
-
if (!prior) throw new Error(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
234
|
-
return { script: prior.script, source: "resume", toolset: prior.toolset };
|
|
235
|
-
})()
|
|
236
|
-
: resolveScript(params.script, params.name, params.args);
|
|
237
|
-
const { script, source, toolset } = resolved;
|
|
238
|
-
const options: ScriptStartOptions = {
|
|
239
|
-
args: params.args,
|
|
240
|
-
background: params.background ?? true,
|
|
241
|
-
maxAgents: params.maxAgents,
|
|
242
|
-
concurrency: params.concurrency,
|
|
243
|
-
agentRetries: params.agentRetries,
|
|
244
|
-
tokenBudget: params.tokenBudget,
|
|
245
|
-
agentTimeoutMs: params.agentTimeoutMs,
|
|
246
|
-
toolset,
|
|
247
|
-
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
248
|
-
signal,
|
|
249
|
-
mainModel: ctx?.model?.id,
|
|
250
|
-
// Foreground checkpoint confirmation only when the run is not
|
|
251
|
-
// background — background runs are headless by contract.
|
|
252
|
-
confirm:
|
|
253
|
-
params.background === false && ctx?.hasUI && ctx.mode === "tui"
|
|
254
|
-
? async (promptText, checkpointOptions) => {
|
|
255
|
-
if (checkpointOptions.kind === "input")
|
|
256
|
-
return ctx.ui.input(promptText, String(checkpointOptions.default ?? ""));
|
|
257
|
-
if (checkpointOptions.kind === "select")
|
|
258
|
-
return ctx.ui.select(promptText, checkpointOptions.choices ?? []);
|
|
259
|
-
return ctx.ui.confirm(promptText, promptText);
|
|
260
|
-
}
|
|
261
|
-
: undefined,
|
|
262
|
-
loadSavedWorkflow: (name) => loadSavedWorkflow(name, sessionCwd) ?? BUILTIN_WORKFLOWS[name]?.script,
|
|
263
|
-
};
|
|
264
|
-
if (params.resumeFromRunId) {
|
|
265
|
-
const replacement = params.script !== undefined || params.name !== undefined ? script : undefined;
|
|
266
|
-
const resumed = await workflowEngine.resume(params.resumeFromRunId, branchEntries(), options, replacement);
|
|
267
|
-
if (!resumed) return textResult(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
268
|
-
if (resumed.background) void deliverBackgroundResult(workflowEngine, resumed.runId);
|
|
269
|
-
return textResult(formatStart(resumed), resumed);
|
|
270
|
-
}
|
|
271
|
-
const result = await workflowEngine.start(script, options);
|
|
272
|
-
if (result.background) {
|
|
273
|
-
// The engine resolves the script's meta name for the delivery header.
|
|
274
|
-
void source;
|
|
275
|
-
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
503
|
+
return textResult(error instanceof Error ? error.message : String(error));
|
|
276
504
|
}
|
|
277
|
-
return textResult(formatStart(result), result);
|
|
278
505
|
},
|
|
279
506
|
}),
|
|
280
507
|
);
|
|
@@ -294,20 +521,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
294
521
|
return new Text(theme.fg("dim", expanded ? text.slice(0, 8_000) : text.slice(0, 1_000)), 0, 0);
|
|
295
522
|
},
|
|
296
523
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
524
|
+
try {
|
|
525
|
+
const rawAction = params.action as string;
|
|
526
|
+
const action = (rawAction === "status" ? "get" : rawAction) as
|
|
527
|
+
| "list"
|
|
528
|
+
| "get"
|
|
529
|
+
| "pause"
|
|
530
|
+
| "resume"
|
|
531
|
+
| "stop"
|
|
532
|
+
| "rm";
|
|
533
|
+
if (!["list", "get", "pause", "resume", "stop", "rm"].includes(action)) {
|
|
534
|
+
throw new Error(`unknown workflow_control action: ${params.action}`);
|
|
535
|
+
}
|
|
536
|
+
const runId = normalizeControlRunId(params as unknown as Record<string, unknown>);
|
|
537
|
+
const workflowEngine = currentEngine();
|
|
538
|
+
const result = await workflowEngine.control(action, runId);
|
|
539
|
+
return textResult(JSON.stringify(result), result);
|
|
540
|
+
} catch (error: unknown) {
|
|
541
|
+
return textResult(error instanceof Error ? error.message : String(error));
|
|
307
542
|
}
|
|
308
|
-
const runId = normalizeControlRunId(params as unknown as Record<string, unknown>);
|
|
309
|
-
const result = await workflowEngine.control(action, runId);
|
|
310
|
-
return textResult(JSON.stringify(result), result);
|
|
311
543
|
},
|
|
312
544
|
}),
|
|
313
545
|
);
|
|
@@ -319,8 +551,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
319
551
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
320
552
|
const [subcommand, ...rest] = tokens;
|
|
321
553
|
const restArgs = rest.join(" ").trim();
|
|
554
|
+
const engineForCommand = (): WorkflowEngine | undefined => {
|
|
555
|
+
try {
|
|
556
|
+
return currentEngine();
|
|
557
|
+
} catch (error: unknown) {
|
|
558
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
322
562
|
if (!subcommand) {
|
|
323
|
-
await showWorkflowNavigator(ctx,
|
|
563
|
+
await showWorkflowNavigator(ctx, currentEngine);
|
|
324
564
|
return;
|
|
325
565
|
}
|
|
326
566
|
if (subcommand === "run") {
|
|
@@ -333,6 +573,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
333
573
|
return;
|
|
334
574
|
}
|
|
335
575
|
if (subcommand === "status") {
|
|
576
|
+
const workflowEngine = engineForCommand();
|
|
577
|
+
if (!workflowEngine) return;
|
|
336
578
|
if (restArgs) {
|
|
337
579
|
const run = workflowEngine.getRun(restArgs);
|
|
338
580
|
if (!run) {
|
|
@@ -352,6 +594,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
352
594
|
return;
|
|
353
595
|
}
|
|
354
596
|
if (subcommand === "watch") {
|
|
597
|
+
const workflowEngine = engineForCommand();
|
|
598
|
+
if (!workflowEngine) return;
|
|
355
599
|
const runId = restArgs || String(workflowEngine.list()[0]?.runId ?? "");
|
|
356
600
|
if (!runId) {
|
|
357
601
|
ctx.ui.notify("No workflow runs to watch.", "warning");
|
|
@@ -362,7 +606,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
362
606
|
return;
|
|
363
607
|
}
|
|
364
608
|
if (subcommand === "stop" || subcommand === "pause" || subcommand === "resume" || subcommand === "rm") {
|
|
365
|
-
const
|
|
609
|
+
const listedEngine = engineForCommand();
|
|
610
|
+
if (!listedEngine) return;
|
|
611
|
+
const firstListed = listedEngine.list()[0]?.runId;
|
|
366
612
|
const runId = resolveControlTarget(
|
|
367
613
|
subcommand,
|
|
368
614
|
restArgs,
|
|
@@ -379,6 +625,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
379
625
|
return;
|
|
380
626
|
}
|
|
381
627
|
try {
|
|
628
|
+
const workflowEngine = engineForCommand();
|
|
629
|
+
if (!workflowEngine) return;
|
|
382
630
|
await workflowEngine.control(subcommand, runId);
|
|
383
631
|
if (subcommand === "resume") void deliverBackgroundResult(workflowEngine, runId);
|
|
384
632
|
ctx.ui.notify(`Workflow ${subcommand} requested: ${runId}`, "info");
|
|
@@ -393,6 +641,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
393
641
|
ctx.ui.notify("/workflows save <name> [runId]", "warning");
|
|
394
642
|
return;
|
|
395
643
|
}
|
|
644
|
+
const workflowEngine = engineForCommand();
|
|
645
|
+
if (!workflowEngine) return;
|
|
396
646
|
// Fallback to most recent run when runId omitted — matches upstream runs.find(r=>r.script)
|
|
397
647
|
const runId = rest[1] ?? String(workflowEngine.list()[0]?.runId ?? "");
|
|
398
648
|
if (!runId) {
|
|
@@ -471,8 +721,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
471
721
|
// is annotated to say so. Arming AUTHORIZES the tool; it never forces a run,
|
|
472
722
|
// and it never opens UI. A user asking a question about workflows still gets
|
|
473
723
|
// an ordinary answer.
|
|
474
|
-
let triggerKeyword: string | undefined = workflowSettings.keywordTriggerWord;
|
|
475
|
-
let keywordTriggerEnabled = workflowSettings.keywordTriggerEnabled;
|
|
476
724
|
const TRIGGER_WORD_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/u;
|
|
477
725
|
const persistTriggerSettings = (): void => {
|
|
478
726
|
workflowSettings = { ...workflowSettings, keywordTriggerWord: triggerKeyword, keywordTriggerEnabled };
|
|
@@ -597,7 +845,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
597
845
|
// unregister or replace a command, so clobbering one is permanent.
|
|
598
846
|
const claimedCommands = new Set((pi.getCommands?.() ?? []).map((command) => command.name));
|
|
599
847
|
for (const name of Object.keys(BUILTIN_WORKFLOWS)) {
|
|
600
|
-
if (claimedCommands.has(name)) continue;
|
|
848
|
+
if (claimedCommands.has(name) || registeredWorkflowCommands.has(name)) continue;
|
|
601
849
|
const descriptor = BUILTIN_WORKFLOWS[name];
|
|
602
850
|
pi.registerCommand(name, {
|
|
603
851
|
description: `Run the ${name} workflow`,
|
|
@@ -625,54 +873,33 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
625
873
|
args: scriptArgs,
|
|
626
874
|
background: true,
|
|
627
875
|
toolset: descriptor.toolset,
|
|
876
|
+
shippedScript: true,
|
|
628
877
|
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
629
|
-
loadSavedWorkflow:
|
|
630
|
-
loadSavedWorkflow(savedName, sessionCwd) ?? BUILTIN_WORKFLOWS[savedName]?.script,
|
|
878
|
+
loadSavedWorkflow: resolveNestedWorkflow,
|
|
631
879
|
};
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
880
|
+
try {
|
|
881
|
+
const workflowEngine = currentEngine();
|
|
882
|
+
const result = await workflowEngine.start(descriptor.script, options);
|
|
883
|
+
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
884
|
+
ctx.ui.notify(`Workflow ${name} started in background: ${result.runId}`, "info");
|
|
885
|
+
} catch (error: unknown) {
|
|
886
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
887
|
+
}
|
|
635
888
|
},
|
|
636
889
|
});
|
|
890
|
+
registeredWorkflowCommands.add(name);
|
|
637
891
|
}
|
|
638
892
|
|
|
639
|
-
// Live progress widget
|
|
640
|
-
//
|
|
641
|
-
const refreshWidget = () => {
|
|
642
|
-
const tui = (pi as unknown as { ui?: { setWidget?: (key: string, content: string[] | undefined) => void } }).ui;
|
|
643
|
-
if (!tui?.setWidget) return;
|
|
644
|
-
const runs = workflowEngine
|
|
645
|
-
.list()
|
|
646
|
-
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
647
|
-
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
648
|
-
const lines = liveWidgetLines(runs, workflowSettings.progressMode, workflowSettings.maxAgentsShown);
|
|
649
|
-
tui.setWidget("pi-workflows", lines.length > 0 ? lines : undefined);
|
|
650
|
-
if (lines.length > 0 && !widgetTimer) {
|
|
651
|
-
widgetTimer = setInterval(() => {
|
|
652
|
-
if (!tui?.setWidget) return;
|
|
653
|
-
const current = workflowEngine
|
|
654
|
-
.list()
|
|
655
|
-
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
656
|
-
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
657
|
-
const next = liveWidgetLines(current, workflowSettings.progressMode, workflowSettings.maxAgentsShown);
|
|
658
|
-
tui.setWidget("pi-workflows", next.length > 0 ? next : undefined);
|
|
659
|
-
if (next.length === 0 && widgetTimer) {
|
|
660
|
-
clearInterval(widgetTimer);
|
|
661
|
-
widgetTimer = undefined;
|
|
662
|
-
}
|
|
663
|
-
}, 2_000);
|
|
664
|
-
} else if (lines.length === 0 && widgetTimer) {
|
|
665
|
-
clearInterval(widgetTimer);
|
|
666
|
-
widgetTimer = undefined;
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
// Refresh after any control action and on run settlement.
|
|
670
|
-
const notifyRunChanged = () => refreshWidget();
|
|
671
|
-
workflowEngine.onRunSettled = notifyRunChanged;
|
|
672
|
-
void notifyRunChanged;
|
|
673
|
-
|
|
893
|
+
// Live progress widget refreshes through the current engine, so its timer
|
|
894
|
+
// and settlement callback remain safe across session replacement.
|
|
674
895
|
const beginBranchQuiesce = (): Promise<{ settled: boolean; pending: string[]; diagnostic?: string }> => {
|
|
675
896
|
if (!branchQuiesce) {
|
|
897
|
+
let workflowEngine: WorkflowEngine;
|
|
898
|
+
try {
|
|
899
|
+
workflowEngine = currentEngine();
|
|
900
|
+
} catch {
|
|
901
|
+
return Promise.resolve({ settled: true, pending: [] });
|
|
902
|
+
}
|
|
676
903
|
branchQuiesce = workflowEngine.quiesceForBranchChange().then((result) => {
|
|
677
904
|
if (!result.settled) {
|
|
678
905
|
console.warn(
|
|
@@ -685,7 +912,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
685
912
|
return branchQuiesce;
|
|
686
913
|
};
|
|
687
914
|
|
|
688
|
-
|
|
915
|
+
pi.events.on("subagents:session_before_tree", () => {
|
|
689
916
|
void beginBranchQuiesce();
|
|
690
917
|
});
|
|
691
918
|
|
|
@@ -699,6 +926,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
699
926
|
} catch {
|
|
700
927
|
return;
|
|
701
928
|
}
|
|
929
|
+
let workflowEngine: WorkflowEngine;
|
|
930
|
+
try {
|
|
931
|
+
workflowEngine = currentEngine();
|
|
932
|
+
} catch {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
702
935
|
workflowEngine.suspendLifecycle();
|
|
703
936
|
try {
|
|
704
937
|
workflowEngine.restore(entries);
|
|
@@ -731,7 +964,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
731
964
|
const run = await engine.waitFor(runId).catch(() => undefined);
|
|
732
965
|
if (!run) return;
|
|
733
966
|
const header = `Workflow ${run.runId} ${run.status}`;
|
|
734
|
-
const
|
|
967
|
+
const state = engine.getState(runId);
|
|
968
|
+
const finalResult = state?.result ?? run.finalResult;
|
|
969
|
+
const body =
|
|
970
|
+
run.error ??
|
|
971
|
+
(finalResult === undefined
|
|
972
|
+
? `completed with ${Object.keys(run.callResults).length} agent call(s).`
|
|
973
|
+
: `result:\n${typeof finalResult === "string" ? finalResult : (JSON.stringify(finalResult, null, 2) ?? String(finalResult))}`);
|
|
735
974
|
const text = `${header}: ${body}`;
|
|
736
975
|
if (deliverResult) {
|
|
737
976
|
try {
|
|
@@ -772,14 +1011,20 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
772
1011
|
|
|
773
1012
|
/** Build a script from a natural-language prompt and run it in background. */
|
|
774
1013
|
const runWorkflowFromPrompt = async (ctx: ExtensionCommandContext, prompt: string): Promise<string | undefined> => {
|
|
775
|
-
// A bounded
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
return answer;`;
|
|
1014
|
+
// A bounded planner → named DAG → synthesis script. Keeping the planner's
|
|
1015
|
+
// output inside the workflow makes /workflows run useful for broad tasks,
|
|
1016
|
+
// while the graph validates dependencies before dispatching any workers.
|
|
1017
|
+
const script = buildAdHocWorkflowScript(prompt);
|
|
780
1018
|
try {
|
|
781
|
-
const
|
|
782
|
-
|
|
1019
|
+
const workflowEngine = currentEngine();
|
|
1020
|
+
const result = await workflowEngine.start(script, {
|
|
1021
|
+
background: true,
|
|
1022
|
+
// We wrote this script, tiers included, so its tier names are a
|
|
1023
|
+
// preference against whatever catalogue this host defines.
|
|
1024
|
+
shippedScript: true,
|
|
1025
|
+
mainModel: ctx.model?.id,
|
|
1026
|
+
});
|
|
1027
|
+
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
783
1028
|
return result.runId;
|
|
784
1029
|
} catch (error: unknown) {
|
|
785
1030
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
@@ -788,7 +1033,7 @@ return answer;`;
|
|
|
788
1033
|
};
|
|
789
1034
|
|
|
790
1035
|
const saveWorkflowFromRun = async (ctx: ExtensionCommandContext, name: string, runId?: string): Promise<void> => {
|
|
791
|
-
const state =
|
|
1036
|
+
const state = currentEngine().getState(runId ?? "");
|
|
792
1037
|
if (!state) {
|
|
793
1038
|
throw new Error(`Workflow run not found: ${runId ?? "(none)"}`);
|
|
794
1039
|
}
|
|
@@ -802,6 +1047,8 @@ return answer;`;
|
|
|
802
1047
|
sessionCwd = ctx.cwd ?? process.cwd();
|
|
803
1048
|
workflowSettings = loadWorkflowSettings(sessionCwd);
|
|
804
1049
|
effortLevel = workflowSettings.effort;
|
|
1050
|
+
triggerKeyword = workflowSettings.keywordTriggerWord;
|
|
1051
|
+
keywordTriggerEnabled = workflowSettings.keywordTriggerEnabled;
|
|
805
1052
|
let initialEntries: SessionEntryLike[];
|
|
806
1053
|
try {
|
|
807
1054
|
initialEntries = ctx.sessionManager.getBranch() as unknown as SessionEntryLike[];
|
|
@@ -819,6 +1066,15 @@ return answer;`;
|
|
|
819
1066
|
|
|
820
1067
|
protocolAbortController = new AbortController();
|
|
821
1068
|
const client = createManagedSpawnClient(pi.events, protocolAbortController.signal, protocolAbortController.signal);
|
|
1069
|
+
protocolError = undefined;
|
|
1070
|
+
protocolProbe = () =>
|
|
1071
|
+
client.checkProtocol?.() ?? Promise.reject(new Error("managed protocol check is unavailable"));
|
|
1072
|
+
protocolCheck = trackProbe(protocolProbe());
|
|
1073
|
+
void protocolCheck.catch((error: unknown) => {
|
|
1074
|
+
if (generation !== lifecycleGeneration) return;
|
|
1075
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1076
|
+
protocolError = `${PROTOCOL_DIAGNOSTIC} Diagnostic: ${detail}`;
|
|
1077
|
+
});
|
|
822
1078
|
|
|
823
1079
|
let recoveredEngine: WorkflowEngine | undefined;
|
|
824
1080
|
let recoveryError: unknown;
|
|
@@ -838,6 +1094,7 @@ return answer;`;
|
|
|
838
1094
|
return [];
|
|
839
1095
|
}
|
|
840
1096
|
},
|
|
1097
|
+
awaitProtocol,
|
|
841
1098
|
);
|
|
842
1099
|
try {
|
|
843
1100
|
const entries =
|
|
@@ -919,36 +1176,39 @@ return answer;`;
|
|
|
919
1176
|
// name clashes first — never overwrite another extension's command.
|
|
920
1177
|
const existingCommands = new Set((pi.getCommands?.() ?? []).map((command) => command.name));
|
|
921
1178
|
for (const savedName of listSavedWorkflows(sessionCwd)) {
|
|
922
|
-
if (existingCommands.has(savedName)) continue;
|
|
923
|
-
|
|
924
|
-
if (!script) continue;
|
|
1179
|
+
if (existingCommands.has(savedName) || registeredWorkflowCommands.has(savedName)) continue;
|
|
1180
|
+
if (!loadSavedWorkflow(savedName, sessionCwd)) continue;
|
|
925
1181
|
pi.registerCommand(savedName, {
|
|
926
1182
|
description: `Run the saved workflow ${savedName}`,
|
|
927
1183
|
handler: async (args: string, commandCtx: ExtensionCommandContext) => {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1184
|
+
try {
|
|
1185
|
+
const workflowEngine = currentEngine();
|
|
1186
|
+
const script = loadSavedWorkflow(savedName, sessionCwd) ?? BUILTIN_WORKFLOWS[savedName]?.script;
|
|
1187
|
+
if (!script) throw new Error(`Saved workflow "${savedName}" is no longer available.`);
|
|
1188
|
+
const options: ScriptStartOptions = {
|
|
1189
|
+
args: args.trim() ? { prompt: args.trim() } : undefined,
|
|
1190
|
+
background: true,
|
|
1191
|
+
mainModel: commandCtx?.model?.id,
|
|
1192
|
+
loadSavedWorkflow: resolveNestedWorkflow,
|
|
1193
|
+
};
|
|
1194
|
+
const result = await workflowEngine.start(script, options);
|
|
1195
|
+
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1196
|
+
commandCtx.ui.notify(`Workflow ${savedName} started in background: ${result.runId}`, "info");
|
|
1197
|
+
} catch (error: unknown) {
|
|
1198
|
+
commandCtx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1199
|
+
}
|
|
937
1200
|
},
|
|
938
1201
|
});
|
|
1202
|
+
registeredWorkflowCommands.add(savedName);
|
|
939
1203
|
}
|
|
940
|
-
|
|
941
|
-
protocolError = undefined;
|
|
942
|
-
protocolCheck = client.checkProtocol?.() ?? Promise.reject(new Error("managed protocol check is unavailable"));
|
|
943
|
-
void protocolCheck.catch((error: unknown) => {
|
|
944
|
-
if (generation !== lifecycleGeneration) return;
|
|
945
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
946
|
-
protocolError = `${PROTOCOL_DIAGNOSTIC} Diagnostic: ${detail}`;
|
|
947
|
-
});
|
|
948
1204
|
});
|
|
949
1205
|
|
|
950
1206
|
pi.on("session_shutdown", async () => {
|
|
951
1207
|
lifecycleGeneration += 1;
|
|
1208
|
+
// Invalidate all long-lived command/tool/widget closures before awaiting
|
|
1209
|
+
// engine quiescence; the old session must not accept another run.
|
|
1210
|
+
active = false;
|
|
1211
|
+
clearWidget();
|
|
952
1212
|
if (widgetTimer) {
|
|
953
1213
|
clearInterval(widgetTimer);
|
|
954
1214
|
widgetTimer = undefined;
|
|
@@ -959,13 +1219,12 @@ return answer;`;
|
|
|
959
1219
|
}
|
|
960
1220
|
protocolAbortController?.abort();
|
|
961
1221
|
protocolAbortController = undefined;
|
|
962
|
-
unsubscribeTreeSignal?.();
|
|
963
|
-
unsubscribeTreeSignal = undefined;
|
|
964
1222
|
closingEngine?.dispose();
|
|
965
1223
|
engine = undefined;
|
|
966
|
-
active = false;
|
|
967
1224
|
branchQuiesce = undefined;
|
|
968
|
-
protocolCheck =
|
|
1225
|
+
protocolCheck = undefined;
|
|
1226
|
+
protocolProbe = undefined;
|
|
1227
|
+
protocolInFlight = undefined;
|
|
969
1228
|
protocolError = undefined;
|
|
970
1229
|
deliverResult = undefined;
|
|
971
1230
|
});
|