@quintinshaw/pi-dynamic-workflows 1.9.0 → 1.9.2
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 +95 -65
- package/dist/agent.d.ts +6 -0
- package/dist/agent.js +16 -0
- package/dist/display.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/run-persistence.d.ts +2 -0
- package/dist/task-panel.d.ts +14 -5
- package/dist/task-panel.js +35 -27
- package/dist/workflow-editor.d.ts +74 -0
- package/dist/workflow-editor.js +228 -0
- package/dist/workflow-manager.d.ts +38 -3
- package/dist/workflow-manager.js +66 -11
- package/dist/workflow-tool.d.ts +7 -0
- package/dist/workflow-tool.js +65 -67
- package/dist/workflow-ui.d.ts +1 -0
- package/dist/workflow-ui.js +27 -5
- package/dist/workflow.d.ts +9 -0
- package/dist/workflow.js +11 -4
- package/extensions/workflow.ts +12 -0
- package/package.json +16 -3
- package/src/agent.ts +16 -0
- package/src/display.ts +2 -0
- package/src/index.ts +13 -2
- package/src/run-persistence.ts +2 -0
- package/src/task-panel.ts +39 -28
- package/src/workflow-editor.ts +252 -0
- package/src/workflow-manager.ts +91 -14
- package/src/workflow-tool.ts +68 -66
- package/src/workflow-ui.ts +27 -5
- package/src/workflow.ts +27 -5
package/dist/workflow-tool.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import {
|
|
4
|
+
import { listAvailableModelSpecs } from "./agent.js";
|
|
5
|
+
import { createToolUpdateWorkflowDisplay, createWorkflowSnapshot, recomputeWorkflowSnapshot, renderWorkflowText, } from "./display.js";
|
|
5
6
|
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
6
|
-
import { parseWorkflowScript
|
|
7
|
+
import { parseWorkflowScript } from "./workflow.js";
|
|
7
8
|
import { WorkflowManager } from "./workflow-manager.js";
|
|
8
9
|
import { createWorkflowStorage } from "./workflow-saved.js";
|
|
10
|
+
/**
|
|
11
|
+
* Per-agent model-routing policy handed to the workflow author (the model). It
|
|
12
|
+
* states the rule and lists the user's currently available models, then lets the
|
|
13
|
+
* author choose each agent's model via opts.model — no hardcoded family mapping.
|
|
14
|
+
*/
|
|
15
|
+
function modelRoutingGuideline() {
|
|
16
|
+
const available = listAvailableModelSpecs();
|
|
17
|
+
const list = available.length
|
|
18
|
+
? `The user's currently available models (route only to these) are: ${available.join(", ")}.`
|
|
19
|
+
: "Use models the user has configured.";
|
|
20
|
+
return [
|
|
21
|
+
"For workflow, decide each agent's model yourself via opts.model, following this policy:",
|
|
22
|
+
"If the user named a specific model, use exactly that.",
|
|
23
|
+
"Otherwise, for exploration/search/inventory/gathering agents, pick a model one tier BELOW the main model in the SAME family (e.g. Claude→Haiku, ChatGPT/GPT→a mini, DeepSeek→a lighter/flash variant), choosing the closest match from the available list.",
|
|
24
|
+
"For analysis/synthesis/judgment/decision/verification agents, omit opts.model so the agent runs on the main model.",
|
|
25
|
+
"Never route to a model that is not in the available list; if no suitable lighter sibling exists, omit opts.model (use the main model).",
|
|
26
|
+
list,
|
|
27
|
+
].join(" ");
|
|
28
|
+
}
|
|
9
29
|
const workflowToolSchema = Type.Object({
|
|
10
30
|
script: Type.String({
|
|
11
31
|
description: [
|
|
@@ -17,7 +37,7 @@ const workflowToolSchema = Type.Object({
|
|
|
17
37
|
}),
|
|
18
38
|
args: Type.Optional(Type.Any({ description: "Optional JSON value exposed to the workflow script as global `args`." })),
|
|
19
39
|
background: Type.Optional(Type.Boolean({
|
|
20
|
-
description: "Run the workflow in the background. Default:
|
|
40
|
+
description: "Run the workflow in the background. Default: true — the tool returns immediately with a run ID, the turn ends so the user isn't blocked, and the result is delivered back into the conversation when it finishes. Set to false only when you need the result inline in this same turn (the call will block until the workflow completes).",
|
|
21
41
|
})),
|
|
22
42
|
maxAgents: Type.Optional(Type.Number({
|
|
23
43
|
description: "Maximum number of agents allowed in this run. Default: 1000.",
|
|
@@ -28,8 +48,12 @@ const workflowToolSchema = Type.Object({
|
|
|
28
48
|
});
|
|
29
49
|
export function createWorkflowTool(options = {}) {
|
|
30
50
|
const storage = options.storage ?? createWorkflowStorage(options.cwd ?? process.cwd());
|
|
31
|
-
const manager = options.manager ??
|
|
32
|
-
|
|
51
|
+
const manager = options.manager ??
|
|
52
|
+
new WorkflowManager({
|
|
53
|
+
cwd: options.cwd,
|
|
54
|
+
concurrency: options.concurrency,
|
|
55
|
+
loadSavedWorkflow: (name) => storage.load(name)?.script,
|
|
56
|
+
});
|
|
33
57
|
return defineTool({
|
|
34
58
|
name: "workflow",
|
|
35
59
|
label: "Workflow",
|
|
@@ -51,36 +75,33 @@ export function createWorkflowTool(options = {}) {
|
|
|
51
75
|
"For workflow, failed agent(), parallel(), or pipeline() branches return null and log the failure unless the workflow is aborted. Check for nulls before synthesizing conclusions.",
|
|
52
76
|
"For workflow, include a final synthesis/assertion agent when combining multiple subagent results; return a compact JSON-serializable value with ok/verdict plus the important outputs.",
|
|
53
77
|
"For workflow, if agent() needs machine-readable output, pass a plain JSON Schema via opts.schema; agent() will return the validated object. Use JSON Schema syntax, not TypeScript or TypeBox constructors.",
|
|
78
|
+
modelRoutingGuideline(),
|
|
54
79
|
"For workflow, do not assume the parent assistant has repository code context inside subagents; include enough task context and relevant paths in each agent prompt.",
|
|
55
|
-
"For workflow,
|
|
80
|
+
"For workflow, runs are background by default: the tool returns immediately with a run ID, the turn ends so the user isn't blocked, and the result is delivered back into the conversation when the run finishes. Pass background: false only when you must use the result inline in this same turn (it will block).",
|
|
56
81
|
"For workflow, you may call `await workflow('saved-name', argsObject)` to run a saved workflow inline and use its result; nesting is one level deep only, and the global 16-concurrent / 1000-total caps hold across the nesting.",
|
|
57
82
|
],
|
|
58
83
|
parameters: workflowToolSchema,
|
|
59
84
|
prepareArguments(args) {
|
|
60
85
|
return normalizeWorkflowToolArgs(args);
|
|
61
86
|
},
|
|
62
|
-
async execute(_toolCallId, params, signal, onUpdate,
|
|
87
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
63
88
|
const script = normalizeWorkflowScript(params.script);
|
|
64
89
|
const parsed = parseWorkflowScript(script);
|
|
65
|
-
// Background execution
|
|
66
|
-
|
|
90
|
+
// Background execution is the default: return immediately so the turn ends
|
|
91
|
+
// and the user isn't blocked. The result is delivered back into the
|
|
92
|
+
// conversation when the run finishes (see installResultDelivery). Only an
|
|
93
|
+
// explicit `background: false` blocks for the result inline.
|
|
94
|
+
if (params.background ?? true) {
|
|
67
95
|
const { runId } = manager.startInBackground(script, params.args);
|
|
68
96
|
return {
|
|
69
|
-
content: [
|
|
70
|
-
{
|
|
71
|
-
type: "text",
|
|
72
|
-
text: [
|
|
73
|
-
`Workflow "${parsed.meta.name}" started in background.`,
|
|
74
|
-
`Run ID: ${runId}`,
|
|
75
|
-
`Use /workflows status ${runId} to check progress.`,
|
|
76
|
-
`Use /workflows stop ${runId} to cancel.`,
|
|
77
|
-
].join("\n"),
|
|
78
|
-
},
|
|
79
|
-
],
|
|
97
|
+
content: [{ type: "text", text: backgroundStartedText(parsed.meta.name, runId) }],
|
|
80
98
|
details: { runId, background: true },
|
|
81
99
|
};
|
|
82
100
|
}
|
|
83
|
-
// Synchronous execution (blocking)
|
|
101
|
+
// Synchronous execution (blocking) — but routed through the manager so the
|
|
102
|
+
// run shows up live in the /workflows navigator and the task panel while it
|
|
103
|
+
// runs, then stays in history afterwards. We still block on the result and
|
|
104
|
+
// return it inline, so the model gets the full output in the same turn.
|
|
84
105
|
let snapshot = createWorkflowSnapshot(parsed.meta);
|
|
85
106
|
const display = createToolUpdateWorkflowDisplay(onUpdate, undefined, {
|
|
86
107
|
key: "workflow",
|
|
@@ -89,56 +110,15 @@ export function createWorkflowTool(options = {}) {
|
|
|
89
110
|
maxLogs: 1,
|
|
90
111
|
showResultPreviews: false,
|
|
91
112
|
});
|
|
92
|
-
const update = () => {
|
|
93
|
-
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
94
|
-
display.update(snapshot);
|
|
95
|
-
};
|
|
96
113
|
let result;
|
|
97
114
|
try {
|
|
98
|
-
result = await
|
|
99
|
-
cwd: options.cwd ?? ctx.cwd,
|
|
100
|
-
args: params.args,
|
|
101
|
-
signal,
|
|
102
|
-
concurrency: options.concurrency,
|
|
115
|
+
result = await manager.runSync(script, params.args, {
|
|
103
116
|
maxAgents: params.maxAgents,
|
|
104
117
|
agentTimeoutMs: params.agentTimeoutMs,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
snapshot
|
|
108
|
-
update();
|
|
109
|
-
},
|
|
110
|
-
onPhase(title) {
|
|
111
|
-
snapshot.currentPhase = title;
|
|
112
|
-
if (!snapshot.phases.includes(title))
|
|
113
|
-
snapshot.phases.push(title);
|
|
114
|
-
update();
|
|
115
|
-
},
|
|
116
|
-
onAgentStart(event) {
|
|
117
|
-
if (signal?.aborted)
|
|
118
|
-
throw new Error("Workflow was aborted");
|
|
119
|
-
snapshot.agents.push({
|
|
120
|
-
id: snapshot.agents.length + 1,
|
|
121
|
-
label: event.label,
|
|
122
|
-
phase: event.phase,
|
|
123
|
-
prompt: event.prompt,
|
|
124
|
-
status: "running",
|
|
125
|
-
});
|
|
126
|
-
update();
|
|
127
|
-
},
|
|
128
|
-
onAgentEnd(event) {
|
|
129
|
-
const agent = [...snapshot.agents]
|
|
130
|
-
.reverse()
|
|
131
|
-
.find((item) => item.label === event.label && item.status === "running");
|
|
132
|
-
if (agent) {
|
|
133
|
-
agent.status = event.result === null ? "error" : "done";
|
|
134
|
-
agent.resultPreview = preview(event.result);
|
|
135
|
-
agent.tokens = event.tokens;
|
|
136
|
-
}
|
|
137
|
-
update();
|
|
138
|
-
},
|
|
139
|
-
onTokenUsage(usage) {
|
|
140
|
-
snapshot.tokenUsage = usage;
|
|
141
|
-
update();
|
|
118
|
+
externalSignal: signal,
|
|
119
|
+
onProgress(live) {
|
|
120
|
+
snapshot = recomputeWorkflowSnapshot(live);
|
|
121
|
+
display.update(snapshot);
|
|
142
122
|
},
|
|
143
123
|
});
|
|
144
124
|
}
|
|
@@ -199,6 +179,24 @@ export function createWorkflowTool(options = {}) {
|
|
|
199
179
|
},
|
|
200
180
|
});
|
|
201
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* The tool result returned when a workflow starts in the background. It both
|
|
184
|
+
* informs the model and tells it to reassure the user: the run continues on its
|
|
185
|
+
* own and the conversation will resume automatically when it finishes, so the
|
|
186
|
+
* user can just wait here (or go do something else).
|
|
187
|
+
*/
|
|
188
|
+
export function backgroundStartedText(name, runId) {
|
|
189
|
+
return [
|
|
190
|
+
`Workflow "${name}" started in the background.`,
|
|
191
|
+
`Run ID: ${runId}`,
|
|
192
|
+
"It keeps running on its own. When it finishes, the result is delivered back",
|
|
193
|
+
"here and the conversation continues automatically — the user does not need to",
|
|
194
|
+
"do anything. Tell the user they can simply wait here for it to finish (it will",
|
|
195
|
+
"resume the conversation by itself), or keep chatting / working on other things",
|
|
196
|
+
"in the meantime; either way the result will come back to this conversation.",
|
|
197
|
+
`They can also track or cancel it with /workflows status ${runId} or /workflows stop ${runId}.`,
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
202
200
|
function normalizeWorkflowToolArgs(args) {
|
|
203
201
|
if (!args || typeof args !== "object")
|
|
204
202
|
throw new Error("workflow requires an object argument with a script string");
|
package/dist/workflow-ui.d.ts
CHANGED
package/dist/workflow-ui.js
CHANGED
|
@@ -25,6 +25,13 @@ const STATUS_ICON = {
|
|
|
25
25
|
skipped: "⊘",
|
|
26
26
|
};
|
|
27
27
|
const PLAIN = { fg: (_c, t) => t, bold: (t) => t };
|
|
28
|
+
/** Short, human-friendly model label: drop the provider prefix for display. */
|
|
29
|
+
function shortModel(model) {
|
|
30
|
+
if (!model)
|
|
31
|
+
return undefined;
|
|
32
|
+
const slash = model.indexOf("/");
|
|
33
|
+
return slash > 0 ? model.slice(slash + 1) : model;
|
|
34
|
+
}
|
|
28
35
|
/** Reads run/phase/agent data from the manager, preferring live snapshots. */
|
|
29
36
|
export class NavigatorModel {
|
|
30
37
|
manager;
|
|
@@ -90,7 +97,7 @@ export class NavigatorModel {
|
|
|
90
97
|
return [];
|
|
91
98
|
return snap.agents
|
|
92
99
|
.filter((a) => (a.phase ?? "(no phase)") === phase)
|
|
93
|
-
.map((a) => ({ id: a.id, label: a.label, status: a.status, phase: a.phase, tokens: a.tokens }));
|
|
100
|
+
.map((a) => ({ id: a.id, label: a.label, status: a.status, phase: a.phase, tokens: a.tokens, model: a.model }));
|
|
94
101
|
}
|
|
95
102
|
agentDetail(runId, agentId) {
|
|
96
103
|
return this.snapshot(runId)?.snapshot.agents.find((a) => a.id === agentId);
|
|
@@ -110,6 +117,7 @@ function persistedToSnapshot(p) {
|
|
|
110
117
|
status: a.status,
|
|
111
118
|
resultPreview: a.result == null ? undefined : String(typeof a.result === "string" ? a.result : JSON.stringify(a.result)),
|
|
112
119
|
error: a.error,
|
|
120
|
+
model: a.model,
|
|
113
121
|
})),
|
|
114
122
|
agentCount: p.agents.length,
|
|
115
123
|
runningCount: p.agents.filter((a) => a.status === "running").length,
|
|
@@ -246,8 +254,9 @@ export function renderNavigator(state, model, width, theme = PLAIN) {
|
|
|
246
254
|
lines.push(theme.bold(`${model.runName(state.runId)} › ${state.phase}`));
|
|
247
255
|
agents.forEach((a, i) => {
|
|
248
256
|
const icon = STATUS_ICON[a.status] ?? "?";
|
|
249
|
-
const
|
|
250
|
-
|
|
257
|
+
const mdl = shortModel(a.model);
|
|
258
|
+
const meta = [mdl, a.tokens ? fmtTokens(a.tokens) : undefined].filter(Boolean).join(" · ");
|
|
259
|
+
lines.push(sel(i, `${icon} ${a.label}${meta ? dim(` ${meta}`) : ""}`));
|
|
251
260
|
});
|
|
252
261
|
}
|
|
253
262
|
else if (state.kind === "detail" && state.runId && state.agentId != null) {
|
|
@@ -256,6 +265,8 @@ export function renderNavigator(state, model, width, theme = PLAIN) {
|
|
|
256
265
|
if (a) {
|
|
257
266
|
const body = [];
|
|
258
267
|
body.push(dim("Status: ") + (a.status ?? ""));
|
|
268
|
+
if (a.model)
|
|
269
|
+
body.push(dim("Model: ") + (shortModel(a.model) ?? ""));
|
|
259
270
|
if (a.error)
|
|
260
271
|
body.push(dim("Error: ") + a.error);
|
|
261
272
|
body.push("", dim("Prompt:"));
|
|
@@ -385,9 +396,20 @@ export function openWorkflowNavigator(pi, manager, ui, opts = {}) {
|
|
|
385
396
|
ui.notify(manager.stop(id) ? `Stopped ${id}` : `Cannot stop ${id}`, "info");
|
|
386
397
|
break;
|
|
387
398
|
}
|
|
388
|
-
case "restart":
|
|
389
|
-
|
|
399
|
+
case "restart": {
|
|
400
|
+
// Restart re-runs the whole workflow from scratch as a fresh
|
|
401
|
+
// background run (per-agent restart isn't meaningful — agents are
|
|
402
|
+
// driven by the script). The new run auto-delivers when it finishes.
|
|
403
|
+
const id = state.activeRunId(model);
|
|
404
|
+
const run = id ? manager.listRuns().find((r) => r.runId === id) : undefined;
|
|
405
|
+
if (!run?.script) {
|
|
406
|
+
ui.notify(id ? `Cannot restart ${id} (no script saved)` : "No run selected to restart", "warning");
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
const { runId: newId } = manager.startInBackground(run.script, run.args);
|
|
410
|
+
ui.notify(`Restarted ${run.workflowName || "workflow"} as ${newId}`, "info");
|
|
390
411
|
break;
|
|
412
|
+
}
|
|
391
413
|
case "save": {
|
|
392
414
|
const id = state.activeRunId(model);
|
|
393
415
|
const run = id ? manager.listRuns().find((r) => r.runId === id) : undefined;
|
package/dist/workflow.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface SharedRuntime {
|
|
|
38
38
|
export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
39
39
|
args?: unknown;
|
|
40
40
|
agent?: Pick<WorkflowAgent, "run">;
|
|
41
|
+
/** The session's main model (provider/id), shown in /workflows for default agents. */
|
|
42
|
+
mainModel?: string;
|
|
41
43
|
concurrency?: number;
|
|
42
44
|
tokenBudget?: number | null;
|
|
43
45
|
signal?: AbortSignal;
|
|
@@ -73,6 +75,7 @@ export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
|
73
75
|
result: unknown;
|
|
74
76
|
tokens?: number;
|
|
75
77
|
worktree?: string;
|
|
78
|
+
model?: string;
|
|
76
79
|
}) => void;
|
|
77
80
|
onTokenUsage?: (usage: {
|
|
78
81
|
input: number;
|
|
@@ -100,6 +103,12 @@ export interface AgentOptions<TSchemaDef extends TSchema | undefined = TSchema |
|
|
|
100
103
|
label?: string;
|
|
101
104
|
phase?: string;
|
|
102
105
|
schema?: TSchemaDef;
|
|
106
|
+
/**
|
|
107
|
+
* Run this agent on a specific model (`provider/modelId` or a bare `modelId`).
|
|
108
|
+
* The workflow author chooses per-agent models per the routing policy in the
|
|
109
|
+
* tool guidelines (e.g. a lighter model for exploration, the main model for
|
|
110
|
+
* analysis). When omitted, the session's main model is used.
|
|
111
|
+
*/
|
|
103
112
|
model?: string;
|
|
104
113
|
isolation?: "worktree";
|
|
105
114
|
agentType?: string;
|
package/dist/workflow.js
CHANGED
|
@@ -76,6 +76,10 @@ export async function runWorkflow(script, options = {}) {
|
|
|
76
76
|
const requestedLabel = agentOptions.label?.trim();
|
|
77
77
|
// Precedence: explicit agentOptions.model > phase model (meta.phases[].model).
|
|
78
78
|
const modelSpec = agentOptions.model ?? resolveModelForPhase(assignedPhase, routingConfig);
|
|
79
|
+
// For display in /workflows: the model this agent runs on — its explicit/phase
|
|
80
|
+
// spec, else the session's main model. The real resolved id overrides this via
|
|
81
|
+
// onModelResolved once the subagent session is created.
|
|
82
|
+
let displayModel = modelSpec ?? options.mainModel;
|
|
79
83
|
// Deterministic resume key: assigned at lexical call time, before the limiter,
|
|
80
84
|
// so parallel()/pipeline() fan-out is reproducible for a fixed script.
|
|
81
85
|
const callIndex = state.callSeq++;
|
|
@@ -86,15 +90,15 @@ export async function runWorkflow(script, options = {}) {
|
|
|
86
90
|
if (cached && cached.hash === callHash) {
|
|
87
91
|
shared.agentCount++;
|
|
88
92
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, shared.agentCount);
|
|
89
|
-
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model:
|
|
90
|
-
options.onAgentEnd?.({ label, phase: assignedPhase, result: cached.result, tokens: 0 });
|
|
93
|
+
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model: displayModel });
|
|
94
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result: cached.result, tokens: 0, model: displayModel });
|
|
91
95
|
return cached.result;
|
|
92
96
|
}
|
|
93
97
|
return limiter(async () => {
|
|
94
98
|
shared.agentCount++;
|
|
95
99
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, shared.agentCount);
|
|
96
100
|
const timeout = agentOptions.timeoutMs ?? agentTimeoutMs;
|
|
97
|
-
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model:
|
|
101
|
+
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model: displayModel });
|
|
98
102
|
// Optional per-agent worktree isolation (deterministic name -> stable resume keys).
|
|
99
103
|
let worktree;
|
|
100
104
|
if (agentOptions.isolation === "worktree") {
|
|
@@ -127,6 +131,9 @@ export async function runWorkflow(script, options = {}) {
|
|
|
127
131
|
instructions: buildAgentInstructions(assignedPhase, agentOptions),
|
|
128
132
|
model: modelSpec,
|
|
129
133
|
cwd: runCwd,
|
|
134
|
+
onModelResolved: (id) => {
|
|
135
|
+
displayModel = id;
|
|
136
|
+
},
|
|
130
137
|
onUsage: (u) => {
|
|
131
138
|
usage = u;
|
|
132
139
|
},
|
|
@@ -134,7 +141,7 @@ export async function runWorkflow(script, options = {}) {
|
|
|
134
141
|
throwIfAborted();
|
|
135
142
|
const tokens = recordTokens(result);
|
|
136
143
|
options.onAgentJournal?.({ index: callIndex, hash: callHash, result });
|
|
137
|
-
options.onAgentEnd?.({ label, phase: assignedPhase, result, tokens, worktree: runCwd });
|
|
144
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result, tokens, worktree: runCwd, model: displayModel });
|
|
138
145
|
return result;
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
package/extensions/workflow.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createWorkflowTool,
|
|
5
5
|
installResultDelivery,
|
|
6
6
|
installTaskPanel,
|
|
7
|
+
installWorkflowEditor,
|
|
7
8
|
registerAllSavedWorkflows,
|
|
8
9
|
registerBuiltinWorkflows,
|
|
9
10
|
registerWorkflowCommands,
|
|
@@ -24,13 +25,24 @@ export default function extension(pi: ExtensionAPI) {
|
|
|
24
25
|
registerAllSavedWorkflows(pi, cwd, storage);
|
|
25
26
|
// Deliver a background run's result into the conversation when it finishes.
|
|
26
27
|
installResultDelivery(pi, manager);
|
|
28
|
+
// "Workflows mode": type `workflow(s)` to arm a forced workflow (animated),
|
|
29
|
+
// Backspace right after the word disarms it. Registers the `input` hook now;
|
|
30
|
+
// the editor itself is installed once the UI is available (session_start).
|
|
31
|
+
let editorInstalled = false;
|
|
27
32
|
|
|
28
33
|
pi.on("session_start", (_event: unknown, ctx: ExtensionContext) => {
|
|
29
34
|
const active = pi.getActiveTools();
|
|
30
35
|
if (!active.includes(workflowTool.name)) {
|
|
31
36
|
pi.setActiveTools([...active, workflowTool.name]);
|
|
32
37
|
}
|
|
38
|
+
// Tell the manager the session's main model so "explore" agents auto-tier
|
|
39
|
+
// down to a lighter same-family sibling (e.g. Claude → Haiku).
|
|
40
|
+
manager.setMainModel(ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
33
41
|
// Live "workflows running" panel below the input (focus + enter to open).
|
|
34
42
|
installTaskPanel(pi, manager, ctx.ui, { storage, cwd });
|
|
43
|
+
if (!editorInstalled) {
|
|
44
|
+
installWorkflowEditor(pi, ctx.ui);
|
|
45
|
+
editorInstalled = true;
|
|
46
|
+
}
|
|
35
47
|
});
|
|
36
48
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quintinshaw/pi-dynamic-workflows",
|
|
3
|
-
"version": "1.9.
|
|
4
|
-
"description": "Claude-Code-style dynamic
|
|
3
|
+
"version": "1.9.2",
|
|
4
|
+
"description": "Claude-Code-style dynamic workflows for Pi — fan a task out across 100s of subagents with real model routing, token/cost accounting, resume, git-worktree isolation, an interactive /workflows TUI, and a real /deep-research.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -33,8 +33,21 @@
|
|
|
33
33
|
"keywords": [
|
|
34
34
|
"pi-package",
|
|
35
35
|
"pi",
|
|
36
|
+
"pi-coding-agent",
|
|
36
37
|
"workflow",
|
|
37
|
-
"
|
|
38
|
+
"workflows",
|
|
39
|
+
"dynamic-workflows",
|
|
40
|
+
"orchestration",
|
|
41
|
+
"subagents",
|
|
42
|
+
"multi-agent",
|
|
43
|
+
"agents",
|
|
44
|
+
"ai-agents",
|
|
45
|
+
"parallel",
|
|
46
|
+
"fan-out",
|
|
47
|
+
"claude-code",
|
|
48
|
+
"deep-research",
|
|
49
|
+
"code-review",
|
|
50
|
+
"llm"
|
|
38
51
|
],
|
|
39
52
|
"pi": {
|
|
40
53
|
"extensions": [
|
package/src/agent.ts
CHANGED
|
@@ -24,6 +24,22 @@ export interface WorkflowAgentOptions {
|
|
|
24
24
|
instructions?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* List the user's currently available models (those with auth configured) as
|
|
29
|
+
* `provider/modelId` specs. Used to tell the workflow author which models it may
|
|
30
|
+
* route agents to. Best-effort: returns [] if the registry can't be built.
|
|
31
|
+
*/
|
|
32
|
+
export function listAvailableModelSpecs(): string[] {
|
|
33
|
+
try {
|
|
34
|
+
const dir = getAgentDir();
|
|
35
|
+
const auth = AuthStorage.create(join(dir, "auth.json"));
|
|
36
|
+
const registry = ModelRegistry.create(auth, join(dir, "models.json"));
|
|
37
|
+
return registry.getAvailable().map((m) => `${m.provider}/${m.id}`);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/** Real token/cost usage for a single subagent run, read from the SDK session. */
|
|
28
44
|
export interface AgentUsage {
|
|
29
45
|
input: number;
|
package/src/display.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { AdversarialReviewConfig } from "./adversarial-review.js";
|
|
2
2
|
export { generateAdversarialReviewWorkflow, generateMultiPerspectiveWorkflow } from "./adversarial-review.js";
|
|
3
3
|
export type { AgentRunOptions, AgentRunResult, WorkflowAgentOptions } from "./agent.js";
|
|
4
|
-
export { WorkflowAgent } from "./agent.js";
|
|
4
|
+
export { listAvailableModelSpecs, WorkflowAgent } from "./agent.js";
|
|
5
5
|
export type { AutoWorkflowConfig } from "./auto-workflow.js";
|
|
6
6
|
export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
|
|
7
7
|
export { registerBuiltinWorkflows } from "./builtin-commands.js";
|
|
@@ -58,12 +58,23 @@ export type {
|
|
|
58
58
|
} from "./workflow.js";
|
|
59
59
|
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
60
60
|
export { registerWorkflowCommands } from "./workflow-commands.js";
|
|
61
|
+
export {
|
|
62
|
+
buildForcedWorkflowPrompt,
|
|
63
|
+
colorizeWorkflow,
|
|
64
|
+
endsWithTrigger,
|
|
65
|
+
hasTrigger,
|
|
66
|
+
installWorkflowEditor,
|
|
67
|
+
RAINBOW,
|
|
68
|
+
tokenizeAnsi,
|
|
69
|
+
WorkflowEditor,
|
|
70
|
+
type WorkflowModeState,
|
|
71
|
+
} from "./workflow-editor.js";
|
|
61
72
|
export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
|
|
62
73
|
export { WorkflowManager } from "./workflow-manager.js";
|
|
63
74
|
export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
|
|
64
75
|
export { createWorkflowStorage } from "./workflow-saved.js";
|
|
65
76
|
export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
|
|
66
|
-
export { createWorkflowTool } from "./workflow-tool.js";
|
|
77
|
+
export { backgroundStartedText, createWorkflowTool } from "./workflow-tool.js";
|
|
67
78
|
export {
|
|
68
79
|
keyToAction,
|
|
69
80
|
type NavAction,
|
package/src/run-persistence.ts
CHANGED
package/src/task-panel.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Background-run UX, mirroring Claude Code:
|
|
3
3
|
* - A live task panel below the input lists in-progress runs while you keep working.
|
|
4
|
-
*
|
|
4
|
+
* It is informational; run /workflows to open the full navigator.
|
|
5
5
|
* - When a background run finishes, its result is delivered back into the
|
|
6
6
|
* conversation so the paused task continues with the outcome.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
11
|
-
import { parseKey } from "@earendil-works/pi-tui";
|
|
12
11
|
import type { ManagedRun, WorkflowManager } from "./workflow-manager.js";
|
|
13
12
|
import type { WorkflowStorage } from "./workflow-saved.js";
|
|
14
|
-
import { openWorkflowNavigator } from "./workflow-ui.js";
|
|
15
13
|
|
|
16
14
|
const RUN_EVENTS = ["agentStart", "agentEnd", "phase", "log", "complete", "error", "stopped", "paused", "resumed"];
|
|
17
15
|
|
|
@@ -26,31 +24,50 @@ function deliverText(run: ManagedRun): string {
|
|
|
26
24
|
r && typeof r.report === "string" && r.report.trim() ? r.report : JSON.stringify(run.result?.result, null, 2);
|
|
27
25
|
const tokens = run.result?.tokenUsage ? ` · ${run.result.tokenUsage.total.toLocaleString()} tokens` : "";
|
|
28
26
|
const agents = run.result?.agentCount ?? run.snapshot.agentCount;
|
|
29
|
-
return
|
|
27
|
+
return [
|
|
28
|
+
`✓ Background workflow "${run.snapshot.name}" finished (${agents} agents${tokens}).`,
|
|
29
|
+
"Continue helping the user based on this result.",
|
|
30
|
+
"",
|
|
31
|
+
body,
|
|
32
|
+
].join("\n");
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* When a background run finishes (or fails), deliver its result back into the
|
|
37
|
+
* conversation AND continue the turn so the assistant can act on it — without
|
|
38
|
+
* blocking the user meanwhile:
|
|
39
|
+
*
|
|
40
|
+
* - `triggerTurn: true` starts a fresh turn when the agent is idle, feeding the
|
|
41
|
+
* result to the model so the paused conversation continues.
|
|
42
|
+
* - `deliverAs: "followUp"` means that if the user is busy in another turn, the
|
|
43
|
+
* result is queued and picked up after that turn finishes — never interrupting.
|
|
44
|
+
*
|
|
45
|
+
* Set up once per extension; idempotent via an internal guard.
|
|
35
46
|
*/
|
|
36
47
|
export function installResultDelivery(pi: ExtensionAPI, manager: WorkflowManager): void {
|
|
37
48
|
if ((manager as unknown as { __deliveryInstalled?: boolean }).__deliveryInstalled) return;
|
|
38
49
|
(manager as unknown as { __deliveryInstalled?: boolean }).__deliveryInstalled = true;
|
|
39
50
|
|
|
51
|
+
const deliver = (content: string) => {
|
|
52
|
+
void pi.sendMessage(
|
|
53
|
+
{ customType: "workflow-result", content, display: true },
|
|
54
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
40
58
|
manager.on("complete", ({ runId }: { runId: string }) => {
|
|
41
59
|
const run = manager.getRun(runId);
|
|
42
|
-
|
|
60
|
+
// Only background/resumed runs are delivered: a foreground (sync) run already
|
|
61
|
+
// returns its result inline as the tool result, so re-delivering would dup it.
|
|
62
|
+
if (run?.background) deliver(deliverText(run));
|
|
43
63
|
});
|
|
44
64
|
manager.on("error", ({ runId, error }: { runId: string; error?: { message?: string } }) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
content: `✗ Workflow ${runId} failed: ${error?.message ?? "unknown error"}`,
|
|
48
|
-
display: true,
|
|
49
|
-
});
|
|
65
|
+
if (!manager.getRun(runId)?.background) return;
|
|
66
|
+
deliver(`✗ Background workflow ${runId} failed: ${error?.message ?? "unknown error"}`);
|
|
50
67
|
});
|
|
51
68
|
}
|
|
52
69
|
|
|
53
|
-
function renderPanel(manager: WorkflowManager, theme: Theme
|
|
70
|
+
function renderPanel(manager: WorkflowManager, theme: Theme): string[] {
|
|
54
71
|
const active = manager.listRuns().filter((r) => r.status === "running" || r.status === "paused");
|
|
55
72
|
if (!active.length) return [];
|
|
56
73
|
const rows = active.map((r) => {
|
|
@@ -61,36 +78,30 @@ function renderPanel(manager: WorkflowManager, theme: Theme, focused: boolean):
|
|
|
61
78
|
const phase = live?.snapshot.currentPhase ? ` · ${live.snapshot.currentPhase}` : "";
|
|
62
79
|
return ` ${icon} ${r.workflowName} ${done}/${agents.length} agents${phase}`;
|
|
63
80
|
});
|
|
64
|
-
const hint =
|
|
65
|
-
? theme.fg("accent", " enter: open · esc: back")
|
|
66
|
-
: theme.fg("dim", " ↓ then enter, or /workflows, to open");
|
|
81
|
+
const hint = theme.fg("dim", " run /workflows to open");
|
|
67
82
|
return [theme.bold(`Workflows running (${active.length}):`), ...rows, hint];
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/**
|
|
71
86
|
* Install the live "workflows running" panel below the editor. Re-rendered on
|
|
72
|
-
* every manager event
|
|
87
|
+
* every manager event. Informational only — the user opens the navigator with
|
|
88
|
+
* /workflows. (`_pi`/`_opts` are kept for signature stability.)
|
|
73
89
|
*/
|
|
74
90
|
export function installTaskPanel(
|
|
75
|
-
|
|
91
|
+
_pi: ExtensionAPI,
|
|
76
92
|
manager: WorkflowManager,
|
|
77
93
|
ui: ExtensionUIContext,
|
|
78
|
-
|
|
94
|
+
_opts: TaskPanelOptions = {},
|
|
79
95
|
): void {
|
|
80
96
|
ui.setWidget(
|
|
81
97
|
"workflow-tasks",
|
|
82
98
|
(tui: TUI, theme: Theme) => {
|
|
83
99
|
const onEvent = () => tui.requestRender();
|
|
84
100
|
for (const ev of RUN_EVENTS) manager.on(ev, onEvent);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const key = parseKey(data);
|
|
90
|
-
if (key === "enter" || key === "return" || key === "right") {
|
|
91
|
-
void openWorkflowNavigator(pi, manager, ui, opts);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
101
|
+
// Purely informational: it lists running runs and re-renders on events. To
|
|
102
|
+
// open the navigator, the user runs /workflows (the panel takes no input).
|
|
103
|
+
const comp: Component & { dispose?(): void } = {
|
|
104
|
+
render: () => renderPanel(manager, theme),
|
|
94
105
|
invalidate: () => {},
|
|
95
106
|
dispose: () => {
|
|
96
107
|
for (const ev of RUN_EVENTS) manager.off(ev, onEvent);
|