@quintinshaw/pi-dynamic-workflows 1.9.1 → 1.9.3
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 +13 -6
- 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 +76 -0
- package/dist/workflow-editor.js +265 -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 +1 -1
- 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 +288 -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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Workflow manager for background execution, pause/resume, and run management.
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import type {
|
|
5
|
+
import type { WorkflowAgent } from "./agent.js";
|
|
6
|
+
import { type WorkflowSnapshot } from "./display.js";
|
|
6
7
|
import { WorkflowError } from "./errors.js";
|
|
7
8
|
import { type PersistedRunState, type RunPersistence, type RunStatus } from "./run-persistence.js";
|
|
8
9
|
import { type JournalEntry, type WorkflowRunResult } from "./workflow.js";
|
|
@@ -19,12 +20,36 @@ export interface ManagedRun {
|
|
|
19
20
|
args?: unknown;
|
|
20
21
|
/** Accumulated agent results for resume (deterministic call index -> result). */
|
|
21
22
|
journal: JournalEntry[];
|
|
23
|
+
/**
|
|
24
|
+
* True when the run was started in the background (or resumed) and the caller is
|
|
25
|
+
* not awaiting its result inline. Only background runs deliver their result back
|
|
26
|
+
* into the conversation; a foreground sync run already returns it as the tool
|
|
27
|
+
* result, so re-delivering would duplicate it.
|
|
28
|
+
*/
|
|
29
|
+
background: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Per-execution options shared by sync, background, and resume runs. */
|
|
32
|
+
export interface ExecOptions {
|
|
33
|
+
/** Replay these journaled agent results for the unchanged prefix (resume). */
|
|
34
|
+
resumeJournal?: Map<number, JournalEntry>;
|
|
35
|
+
/** Cap on total agents for this run. */
|
|
36
|
+
maxAgents?: number;
|
|
37
|
+
/** Per-agent timeout in milliseconds. */
|
|
38
|
+
agentTimeoutMs?: number;
|
|
39
|
+
/** Host signal (e.g. tool/Esc) that should abort this run when fired. */
|
|
40
|
+
externalSignal?: AbortSignal;
|
|
41
|
+
/** Called with the live snapshot on every progress event. */
|
|
42
|
+
onProgress?: (snapshot: WorkflowSnapshot) => void;
|
|
22
43
|
}
|
|
23
44
|
export interface WorkflowManagerOptions {
|
|
24
45
|
cwd?: string;
|
|
25
46
|
concurrency?: number;
|
|
26
47
|
/** Resolve a saved-workflow name to its script, enabling nested `workflow('name')`. */
|
|
27
48
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
49
|
+
/** Inject a custom agent runner (tests); defaults to a real subagent session. */
|
|
50
|
+
agent?: Pick<WorkflowAgent, "run">;
|
|
51
|
+
/** The session's main model (provider/id), for auto-tiering explore agents. */
|
|
52
|
+
mainModel?: string;
|
|
28
53
|
}
|
|
29
54
|
export declare class WorkflowManager extends EventEmitter {
|
|
30
55
|
private runs;
|
|
@@ -32,7 +57,12 @@ export declare class WorkflowManager extends EventEmitter {
|
|
|
32
57
|
private cwd;
|
|
33
58
|
private concurrency;
|
|
34
59
|
private loadSavedWorkflow?;
|
|
60
|
+
private agent?;
|
|
61
|
+
/** The session's main model (provider/id), for auto-tiering explore agents. */
|
|
62
|
+
private mainModel?;
|
|
35
63
|
constructor(options?: WorkflowManagerOptions);
|
|
64
|
+
/** Set the session's main model (provider/id). Used to auto-tier explore agents. */
|
|
65
|
+
setMainModel(spec: string | undefined): void;
|
|
36
66
|
/**
|
|
37
67
|
* Start a workflow in the background.
|
|
38
68
|
* Returns immediately with a run ID; the workflow executes asynchronously.
|
|
@@ -42,9 +72,14 @@ export declare class WorkflowManager extends EventEmitter {
|
|
|
42
72
|
promise: Promise<WorkflowRunResult>;
|
|
43
73
|
};
|
|
44
74
|
/**
|
|
45
|
-
* Execute a workflow synchronously (blocking)
|
|
75
|
+
* Execute a workflow synchronously (blocking) while still tracking it like a
|
|
76
|
+
* background run, so the `/workflows` navigator and the live task panel see it.
|
|
77
|
+
* `onProgress` fires on every progress event with the current snapshot, letting
|
|
78
|
+
* a caller (e.g. the workflow tool) drive its own inline display.
|
|
46
79
|
*/
|
|
47
|
-
runSync(script: string, args?: unknown): Promise<WorkflowRunResult>;
|
|
80
|
+
runSync(script: string, args?: unknown, exec?: ExecOptions): Promise<WorkflowRunResult>;
|
|
81
|
+
/** Build a fresh managed run with an empty snapshot. */
|
|
82
|
+
private createManaged;
|
|
48
83
|
private executeRun;
|
|
49
84
|
private persistRun;
|
|
50
85
|
/**
|
package/dist/workflow-manager.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Workflow manager for background execution, pause/resume, and run management.
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
+
import { preview } from "./display.js";
|
|
5
6
|
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
6
7
|
import { createRunPersistence, generateRunId, } from "./run-persistence.js";
|
|
7
8
|
import { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
@@ -11,13 +12,22 @@ export class WorkflowManager extends EventEmitter {
|
|
|
11
12
|
cwd;
|
|
12
13
|
concurrency;
|
|
13
14
|
loadSavedWorkflow;
|
|
15
|
+
agent;
|
|
16
|
+
/** The session's main model (provider/id), for auto-tiering explore agents. */
|
|
17
|
+
mainModel;
|
|
14
18
|
constructor(options = {}) {
|
|
15
19
|
super();
|
|
16
20
|
this.cwd = options.cwd ?? process.cwd();
|
|
17
21
|
this.concurrency = options.concurrency ?? 8;
|
|
18
22
|
this.loadSavedWorkflow = options.loadSavedWorkflow;
|
|
23
|
+
this.agent = options.agent;
|
|
24
|
+
this.mainModel = options.mainModel;
|
|
19
25
|
this.persistence = createRunPersistence(this.cwd);
|
|
20
26
|
}
|
|
27
|
+
/** Set the session's main model (provider/id). Used to auto-tier explore agents. */
|
|
28
|
+
setMainModel(spec) {
|
|
29
|
+
this.mainModel = spec;
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Start a workflow in the background.
|
|
23
33
|
* Returns immediately with a run ID; the workflow executes asynchronously.
|
|
@@ -45,6 +55,7 @@ export class WorkflowManager extends EventEmitter {
|
|
|
45
55
|
script,
|
|
46
56
|
args,
|
|
47
57
|
journal: [],
|
|
58
|
+
background: true,
|
|
48
59
|
};
|
|
49
60
|
this.runs.set(runId, managed);
|
|
50
61
|
// Persist initial state
|
|
@@ -65,14 +76,24 @@ export class WorkflowManager extends EventEmitter {
|
|
|
65
76
|
return { runId, promise };
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
68
|
-
* Execute a workflow synchronously (blocking)
|
|
79
|
+
* Execute a workflow synchronously (blocking) while still tracking it like a
|
|
80
|
+
* background run, so the `/workflows` navigator and the live task panel see it.
|
|
81
|
+
* `onProgress` fires on every progress event with the current snapshot, letting
|
|
82
|
+
* a caller (e.g. the workflow tool) drive its own inline display.
|
|
69
83
|
*/
|
|
70
|
-
async runSync(script, args) {
|
|
71
|
-
const
|
|
72
|
-
|
|
84
|
+
async runSync(script, args, exec = {}) {
|
|
85
|
+
const managed = this.createManaged(script, args);
|
|
86
|
+
this.runs.set(managed.runId, managed);
|
|
87
|
+
// Persist the initial state immediately so listRuns()/the task panel can see
|
|
88
|
+
// the run the moment it starts, not only after the first agent journals.
|
|
89
|
+
this.persistRun(managed);
|
|
90
|
+
return this.executeRun(managed, script, args, exec);
|
|
91
|
+
}
|
|
92
|
+
/** Build a fresh managed run with an empty snapshot. */
|
|
93
|
+
createManaged(script, args) {
|
|
73
94
|
const parsed = parseWorkflowScript(script);
|
|
74
|
-
|
|
75
|
-
runId,
|
|
95
|
+
return {
|
|
96
|
+
runId: generateRunId(),
|
|
76
97
|
status: "running",
|
|
77
98
|
snapshot: {
|
|
78
99
|
name: parsed.meta.name,
|
|
@@ -85,22 +106,34 @@ export class WorkflowManager extends EventEmitter {
|
|
|
85
106
|
doneCount: 0,
|
|
86
107
|
errorCount: 0,
|
|
87
108
|
},
|
|
88
|
-
controller,
|
|
109
|
+
controller: new AbortController(),
|
|
89
110
|
startedAt: new Date(),
|
|
90
111
|
script,
|
|
91
112
|
args,
|
|
92
113
|
journal: [],
|
|
114
|
+
background: false,
|
|
93
115
|
};
|
|
94
|
-
this.runs.set(runId, managed);
|
|
95
|
-
return this.executeRun(managed, script, args);
|
|
96
116
|
}
|
|
97
|
-
async executeRun(managed, script, args,
|
|
117
|
+
async executeRun(managed, script, args, exec = {}) {
|
|
118
|
+
const { resumeJournal, maxAgents, agentTimeoutMs, externalSignal, onProgress } = exec;
|
|
119
|
+
const progress = () => onProgress?.(managed.snapshot);
|
|
120
|
+
// Let a host abort (e.g. Esc during a blocking tool call) cancel this run.
|
|
121
|
+
if (externalSignal) {
|
|
122
|
+
if (externalSignal.aborted)
|
|
123
|
+
managed.controller.abort();
|
|
124
|
+
else
|
|
125
|
+
externalSignal.addEventListener("abort", () => managed.controller.abort(), { once: true });
|
|
126
|
+
}
|
|
98
127
|
try {
|
|
99
128
|
const result = await runWorkflow(script, {
|
|
100
129
|
cwd: this.cwd,
|
|
101
130
|
args,
|
|
131
|
+
agent: this.agent,
|
|
132
|
+
mainModel: this.mainModel,
|
|
102
133
|
signal: managed.controller.signal,
|
|
103
134
|
concurrency: this.concurrency,
|
|
135
|
+
maxAgents,
|
|
136
|
+
agentTimeoutMs,
|
|
104
137
|
loadSavedWorkflow: this.loadSavedWorkflow,
|
|
105
138
|
resumeJournal,
|
|
106
139
|
resumeFromRunId: resumeJournal ? managed.runId : undefined,
|
|
@@ -113,6 +146,7 @@ export class WorkflowManager extends EventEmitter {
|
|
|
113
146
|
onLog: (message) => {
|
|
114
147
|
managed.snapshot.logs.push(message);
|
|
115
148
|
this.emit("log", { runId: managed.runId, message });
|
|
149
|
+
progress();
|
|
116
150
|
},
|
|
117
151
|
onPhase: (title) => {
|
|
118
152
|
managed.snapshot.currentPhase = title;
|
|
@@ -120,6 +154,7 @@ export class WorkflowManager extends EventEmitter {
|
|
|
120
154
|
managed.snapshot.phases.push(title);
|
|
121
155
|
}
|
|
122
156
|
this.emit("phase", { runId: managed.runId, title });
|
|
157
|
+
progress();
|
|
123
158
|
},
|
|
124
159
|
onAgentStart: (event) => {
|
|
125
160
|
managed.snapshot.agents.push({
|
|
@@ -128,8 +163,10 @@ export class WorkflowManager extends EventEmitter {
|
|
|
128
163
|
phase: event.phase,
|
|
129
164
|
prompt: event.prompt,
|
|
130
165
|
status: "running",
|
|
166
|
+
model: event.model,
|
|
131
167
|
});
|
|
132
168
|
this.emit("agentStart", { runId: managed.runId, ...event });
|
|
169
|
+
progress();
|
|
133
170
|
},
|
|
134
171
|
onAgentEnd: (event) => {
|
|
135
172
|
const agent = [...managed.snapshot.agents]
|
|
@@ -137,8 +174,18 @@ export class WorkflowManager extends EventEmitter {
|
|
|
137
174
|
.find((a) => a.label === event.label && a.status === "running");
|
|
138
175
|
if (agent) {
|
|
139
176
|
agent.status = event.result === null ? "error" : "done";
|
|
177
|
+
agent.resultPreview = preview(event.result);
|
|
178
|
+
agent.tokens = event.tokens;
|
|
179
|
+
if (event.model)
|
|
180
|
+
agent.model = event.model;
|
|
140
181
|
}
|
|
141
182
|
this.emit("agentEnd", { runId: managed.runId, ...event });
|
|
183
|
+
progress();
|
|
184
|
+
},
|
|
185
|
+
onTokenUsage: (usage) => {
|
|
186
|
+
managed.snapshot.tokenUsage = usage;
|
|
187
|
+
this.emit("tokenUsage", { runId: managed.runId, usage });
|
|
188
|
+
progress();
|
|
142
189
|
},
|
|
143
190
|
});
|
|
144
191
|
managed.status = "completed";
|
|
@@ -184,6 +231,13 @@ export class WorkflowManager extends EventEmitter {
|
|
|
184
231
|
})),
|
|
185
232
|
logs: managed.snapshot.logs,
|
|
186
233
|
result: managed.result?.result,
|
|
234
|
+
tokenUsage: managed.snapshot.tokenUsage
|
|
235
|
+
? {
|
|
236
|
+
input: managed.snapshot.tokenUsage.input,
|
|
237
|
+
output: managed.snapshot.tokenUsage.output,
|
|
238
|
+
total: managed.snapshot.tokenUsage.total,
|
|
239
|
+
}
|
|
240
|
+
: undefined,
|
|
187
241
|
startedAt: managed.startedAt.toISOString(),
|
|
188
242
|
updatedAt: new Date().toISOString(),
|
|
189
243
|
completedAt: managed.status === "completed" ? new Date().toISOString() : undefined,
|
|
@@ -233,12 +287,13 @@ export class WorkflowManager extends EventEmitter {
|
|
|
233
287
|
script: persisted.script,
|
|
234
288
|
args: persisted.args,
|
|
235
289
|
journal: persisted.journal ?? [],
|
|
290
|
+
background: true,
|
|
236
291
|
};
|
|
237
292
|
this.runs.set(runId, managed);
|
|
238
293
|
const resumeJournal = new Map((persisted.journal ?? []).map((e) => [e.index, e]));
|
|
239
294
|
this.emit("resumed", { runId });
|
|
240
295
|
// Run in the background; executeRun records status/errors on the managed run.
|
|
241
|
-
void this.executeRun(managed, persisted.script, persisted.args, resumeJournal).catch(() => { });
|
|
296
|
+
void this.executeRun(managed, persisted.script, persisted.args, { resumeJournal }).catch(() => { });
|
|
242
297
|
return true;
|
|
243
298
|
}
|
|
244
299
|
/**
|
package/dist/workflow-tool.d.ts
CHANGED
|
@@ -25,4 +25,11 @@ export interface WorkflowToolOptions {
|
|
|
25
25
|
storage?: WorkflowStorage;
|
|
26
26
|
}
|
|
27
27
|
export declare function createWorkflowTool(options?: WorkflowToolOptions): ToolDefinition<typeof workflowToolSchema, any>;
|
|
28
|
+
/**
|
|
29
|
+
* The tool result returned when a workflow starts in the background. It both
|
|
30
|
+
* informs the model and tells it to reassure the user: the run continues on its
|
|
31
|
+
* own and the conversation will resume automatically when it finishes, so the
|
|
32
|
+
* user can just wait here (or go do something else).
|
|
33
|
+
*/
|
|
34
|
+
export declare function backgroundStartedText(name: string, runId: string): string;
|
|
28
35
|
export {};
|
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
|
}
|