@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Workflows mode" input affordance, à la a smart input box:
|
|
3
|
+
*
|
|
4
|
+
* - While the editor text contains the word `workflow`/`workflows`, those letters
|
|
5
|
+
* render as a flowing rainbow, signalling that submitting will engage a workflow.
|
|
6
|
+
* - Pressing Backspace immediately after such a word toggles the highlight OFF
|
|
7
|
+
* (the word stays, but turns plain white) — a non-destructive "don't run a
|
|
8
|
+
* workflow after all". Re-typing a fresh trigger word turns it back on.
|
|
9
|
+
* - When the highlight is ON at submit time, the user's message is transformed to
|
|
10
|
+
* instruct Pi to actually run the workflow tool.
|
|
11
|
+
*
|
|
12
|
+
* Implementation: we replace the core editor with a thin subclass of the exported
|
|
13
|
+
* `CustomEditor` (which itself extends pi-tui's `Editor`), overriding only
|
|
14
|
+
* `render()` (to colorize) and `handleInput()` (for the Backspace toggle). All
|
|
15
|
+
* other editor behavior — history, autocomplete, paste, undo, multiline — is
|
|
16
|
+
* inherited untouched.
|
|
17
|
+
*/
|
|
18
|
+
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
// A trigger is `workflow`/`workflows` (substring, case-insensitive) that is NOT
|
|
20
|
+
// immediately preceded by `/` — so a slash command like `/workflows` or `/workflow`
|
|
21
|
+
// is left alone (not colored, not armed).
|
|
22
|
+
/** Matches a trigger anywhere in the text. */
|
|
23
|
+
const TRIGGER = /(?<!\/)workflows?/i;
|
|
24
|
+
/** Global variant for finding every occurrence to colorize. */
|
|
25
|
+
const TRIGGER_G = /(?<!\/)workflows?/gi;
|
|
26
|
+
/** True when the text immediately before the cursor ends with a trigger word. */
|
|
27
|
+
const TRIGGER_AT_END = /(?<!\/)workflows?$/i;
|
|
28
|
+
/** 256-color ring cycling through the spectrum — shifted by a tick to "flow". */
|
|
29
|
+
export const RAINBOW = [
|
|
30
|
+
196, 160, 202, 166, 208, 172, 214, 178, 220, 184, 226, 190, 118, 82, 46, 47, 48, 49, 50, 51, 45, 39, 33, 27, 21, 57,
|
|
31
|
+
93, 129, 165, 201, 198, 197,
|
|
32
|
+
];
|
|
33
|
+
export function hasTrigger(text) {
|
|
34
|
+
return TRIGGER.test(text);
|
|
35
|
+
}
|
|
36
|
+
export function endsWithTrigger(textBeforeCursor) {
|
|
37
|
+
return TRIGGER_AT_END.test(textBeforeCursor);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Split a rendered line into ANSI-escape tokens (passed through verbatim) and
|
|
41
|
+
* single visible-character tokens. Handles CSI sequences (`\x1b[…m`, e.g. the
|
|
42
|
+
* cursor's inverse-video) and APC/OSC string sequences (e.g. the zero-width
|
|
43
|
+
* `CURSOR_MARKER` = `\x1b_pi:c\x07`) so colorization never corrupts them.
|
|
44
|
+
*/
|
|
45
|
+
export function tokenizeAnsi(line) {
|
|
46
|
+
const tokens = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < line.length) {
|
|
49
|
+
if (line[i] === "\x1b") {
|
|
50
|
+
let j = i + 1;
|
|
51
|
+
const next = line[j];
|
|
52
|
+
if (next === "[") {
|
|
53
|
+
// CSI: ends at a final byte in 0x40–0x7e.
|
|
54
|
+
j++;
|
|
55
|
+
while (j < line.length && !(line[j] >= "@" && line[j] <= "~"))
|
|
56
|
+
j++;
|
|
57
|
+
j++;
|
|
58
|
+
}
|
|
59
|
+
else if (next === "]" || next === "_" || next === "P" || next === "^") {
|
|
60
|
+
// String sequence: ends at BEL (\x07) or ST (\x1b\\).
|
|
61
|
+
j++;
|
|
62
|
+
while (j < line.length && line[j] !== "\x07" && !(line[j] === "\x1b" && line[j + 1] === "\\"))
|
|
63
|
+
j++;
|
|
64
|
+
if (line[j] === "\x07")
|
|
65
|
+
j++;
|
|
66
|
+
else if (line[j] === "\x1b")
|
|
67
|
+
j += 2;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
j++; // lone ESC + one byte
|
|
71
|
+
}
|
|
72
|
+
tokens.push({ esc: line.slice(i, j) });
|
|
73
|
+
i = j;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
tokens.push({ ch: line[i] });
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return tokens;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Colorize every `workflow`/`workflows` occurrence in a rendered line with a
|
|
84
|
+
* flowing rainbow, leaving all ANSI escapes (cursor, markers) intact. Returns the
|
|
85
|
+
* line unchanged when it contains no trigger.
|
|
86
|
+
*/
|
|
87
|
+
export function colorizeWorkflow(line, tick, palette = RAINBOW) {
|
|
88
|
+
const tokens = tokenizeAnsi(line);
|
|
89
|
+
const visible = tokens
|
|
90
|
+
.filter((t) => t.ch !== undefined)
|
|
91
|
+
.map((t) => t.ch)
|
|
92
|
+
.join("");
|
|
93
|
+
if (!TRIGGER.test(visible))
|
|
94
|
+
return line;
|
|
95
|
+
const ranges = [];
|
|
96
|
+
TRIGGER_G.lastIndex = 0;
|
|
97
|
+
for (let m = TRIGGER_G.exec(visible); m; m = TRIGGER_G.exec(visible)) {
|
|
98
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
99
|
+
}
|
|
100
|
+
const inRange = (idx) => ranges.some(([s, e]) => idx >= s && idx < e);
|
|
101
|
+
let out = "";
|
|
102
|
+
let vi = 0;
|
|
103
|
+
for (const t of tokens) {
|
|
104
|
+
if (t.esc !== undefined) {
|
|
105
|
+
out += t.esc;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (inRange(vi)) {
|
|
109
|
+
const color = palette[(vi + tick) % palette.length];
|
|
110
|
+
// Reset only the foreground (39) afterwards so a surrounding inverse-video
|
|
111
|
+
// (the cursor) is preserved.
|
|
112
|
+
out += `\x1b[38;5;${color}m${t.ch}\x1b[39m`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out += t.ch ?? "";
|
|
116
|
+
}
|
|
117
|
+
vi++;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Backspace arrives as DEL (0x7f) or BS (0x08) depending on the terminal. */
|
|
122
|
+
function isBackspace(data) {
|
|
123
|
+
return data === "\x7f" || data === "\b";
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Editor that paints the trigger words and owns the on/off toggle. Reads/writes
|
|
127
|
+
* `state.active` so the extension's `input` handler can decide whether to force a
|
|
128
|
+
* workflow at submit time.
|
|
129
|
+
*/
|
|
130
|
+
export class WorkflowEditor extends CustomEditor {
|
|
131
|
+
modeState;
|
|
132
|
+
tick = 0;
|
|
133
|
+
timer;
|
|
134
|
+
/** Toggled off by Backspace-after-word; re-armed when a fresh trigger appears. */
|
|
135
|
+
disabled = false;
|
|
136
|
+
wasTriggered = false;
|
|
137
|
+
constructor(tui, theme, keybindings, modeState) {
|
|
138
|
+
super(tui, theme, keybindings);
|
|
139
|
+
this.modeState = modeState;
|
|
140
|
+
}
|
|
141
|
+
/** Highlighted/armed: a trigger is present and the user hasn't toggled it off. */
|
|
142
|
+
isActive() {
|
|
143
|
+
return !this.disabled && hasTrigger(this.getText());
|
|
144
|
+
}
|
|
145
|
+
handleInput(data) {
|
|
146
|
+
// First Backspace right after a trigger word disarms (non-destructive).
|
|
147
|
+
if (isBackspace(data) && this.isActive() && this.cursorAfterTrigger()) {
|
|
148
|
+
this.disabled = true;
|
|
149
|
+
this.syncState();
|
|
150
|
+
this.tui.requestRender();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const before = this.getText();
|
|
154
|
+
super.handleInput(data);
|
|
155
|
+
const after = this.getText();
|
|
156
|
+
if (after !== before) {
|
|
157
|
+
const now = hasTrigger(after);
|
|
158
|
+
// A freshly typed trigger re-arms a previously disabled box.
|
|
159
|
+
if (now && !this.wasTriggered)
|
|
160
|
+
this.disabled = false;
|
|
161
|
+
this.wasTriggered = now;
|
|
162
|
+
}
|
|
163
|
+
this.syncState();
|
|
164
|
+
}
|
|
165
|
+
render(width) {
|
|
166
|
+
const lines = super.render(width);
|
|
167
|
+
// Keep the shared state current even for non-keystroke changes (history
|
|
168
|
+
// recall, programmatic setText) so the submit hook reads the right value.
|
|
169
|
+
this.syncState();
|
|
170
|
+
this.reconcileAnimation();
|
|
171
|
+
if (!this.isActive() || lines.length === 0)
|
|
172
|
+
return lines;
|
|
173
|
+
// First and last lines are the editor's horizontal borders; only the text
|
|
174
|
+
// lines in between are colorized.
|
|
175
|
+
return lines.map((ln, i) => (i === 0 || i === lines.length - 1 ? ln : colorizeWorkflow(ln, this.tick)));
|
|
176
|
+
}
|
|
177
|
+
/** Absolute text before the cursor, used to detect "right after the word". */
|
|
178
|
+
cursorAfterTrigger() {
|
|
179
|
+
const lines = this.getLines();
|
|
180
|
+
const { line, col } = this.getCursor();
|
|
181
|
+
const before = lines.slice(0, line).join("\n") + (line > 0 ? "\n" : "") + (lines[line] ?? "").slice(0, col);
|
|
182
|
+
return endsWithTrigger(before);
|
|
183
|
+
}
|
|
184
|
+
syncState() {
|
|
185
|
+
this.modeState.active = this.isActive();
|
|
186
|
+
}
|
|
187
|
+
reconcileAnimation() {
|
|
188
|
+
const shouldRun = this.isActive() && this.focused;
|
|
189
|
+
if (shouldRun && !this.timer) {
|
|
190
|
+
this.timer = setInterval(() => {
|
|
191
|
+
this.tick = (this.tick + 1) % (RAINBOW.length * 6);
|
|
192
|
+
this.tui.requestRender();
|
|
193
|
+
}, 90);
|
|
194
|
+
// Don't keep the process alive for the animation.
|
|
195
|
+
this.timer.unref?.();
|
|
196
|
+
}
|
|
197
|
+
else if (!shouldRun && this.timer) {
|
|
198
|
+
clearInterval(this.timer);
|
|
199
|
+
this.timer = undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** The directive appended to a submitted message when workflows mode is armed. */
|
|
204
|
+
export function buildForcedWorkflowPrompt(text) {
|
|
205
|
+
return [
|
|
206
|
+
text,
|
|
207
|
+
"",
|
|
208
|
+
"[workflows mode] The user armed workflows mode for this message. Handle it by",
|
|
209
|
+
"running the `workflow` tool: decompose the request and orchestrate subagents",
|
|
210
|
+
"with a workflow script, rather than answering directly.",
|
|
211
|
+
].join("\n");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Install the workflows-mode editor and the submit-time forcing hook.
|
|
215
|
+
* Call once with the UI context (e.g. in `session_start`).
|
|
216
|
+
*/
|
|
217
|
+
export function installWorkflowEditor(pi, ui) {
|
|
218
|
+
const state = { active: false };
|
|
219
|
+
ui.setEditorComponent((tui, theme, keybindings) => new WorkflowEditor(tui, theme, keybindings, state));
|
|
220
|
+
// When armed at submit time, rewrite the user's message to force a workflow.
|
|
221
|
+
pi.on("input", (event) => {
|
|
222
|
+
if (event.source !== "interactive" || !state.active || !event.text)
|
|
223
|
+
return { action: "continue" };
|
|
224
|
+
state.active = false; // consume the arm for this submission
|
|
225
|
+
return { action: "transform", text: buildForcedWorkflowPrompt(event.text) };
|
|
226
|
+
});
|
|
227
|
+
return state;
|
|
228
|
+
}
|
|
@@ -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 {};
|