@quintinshaw/pi-dynamic-workflows 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/dist/adversarial-review.d.ts +20 -0
- package/dist/adversarial-review.js +87 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +90 -0
- package/dist/auto-workflow.d.ts +26 -0
- package/dist/auto-workflow.js +121 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +17 -0
- package/dist/deep-research.d.ts +22 -0
- package/dist/deep-research.js +110 -0
- package/dist/display.d.ts +62 -0
- package/dist/display.js +163 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +63 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +67 -0
- package/dist/model-routing.d.ts +33 -0
- package/dist/model-routing.js +57 -0
- package/dist/run-persistence.d.ts +53 -0
- package/dist/run-persistence.js +78 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/workflow-manager.d.ts +74 -0
- package/dist/workflow-manager.js +241 -0
- package/dist/workflow-saved.d.ts +35 -0
- package/dist/workflow-saved.js +91 -0
- package/dist/workflow-tool.d.ts +22 -0
- package/dist/workflow-tool.js +216 -0
- package/dist/workflow.d.ts +75 -0
- package/dist/workflow.js +364 -0
- package/extensions/workflow.ts +14 -0
- package/package.json +70 -0
- package/src/adversarial-review.ts +107 -0
- package/src/agent.ts +135 -0
- package/src/auto-workflow.ts +146 -0
- package/src/config.ts +24 -0
- package/src/deep-research.ts +128 -0
- package/src/display.ts +236 -0
- package/src/errors.ts +85 -0
- package/src/index.ts +55 -0
- package/src/logger.ts +89 -0
- package/src/model-routing.ts +80 -0
- package/src/run-persistence.ts +132 -0
- package/src/structured-output.ts +47 -0
- package/src/workflow-manager.ts +294 -0
- package/src/workflow-saved.ts +131 -0
- package/src/workflow-tool.ts +254 -0
- package/src/workflow.ts +492 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
createToolUpdateWorkflowDisplay,
|
|
6
|
+
createWorkflowSnapshot,
|
|
7
|
+
preview,
|
|
8
|
+
recomputeWorkflowSnapshot,
|
|
9
|
+
renderWorkflowText,
|
|
10
|
+
type WorkflowSnapshot,
|
|
11
|
+
} from "./display.js";
|
|
12
|
+
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
13
|
+
import { parseWorkflowScript, runWorkflow, type WorkflowRunResult } from "./workflow.js";
|
|
14
|
+
import { WorkflowManager } from "./workflow-manager.js";
|
|
15
|
+
import { createWorkflowStorage } from "./workflow-saved.js";
|
|
16
|
+
|
|
17
|
+
const workflowToolSchema = Type.Object({
|
|
18
|
+
script: Type.String({
|
|
19
|
+
description: [
|
|
20
|
+
"Required raw JavaScript workflow script, with no Markdown fences.",
|
|
21
|
+
"First statement: export const meta = { name: 'short_snake_case', description: 'non-empty description', phases: [{ title: 'Phase' }] }",
|
|
22
|
+
"Use phase('Name'), agent(prompt, opts), parallel(arrayOfFunctions), pipeline(items, ...stages), log(message), args, and budget. The workflow must call agent() at least once.",
|
|
23
|
+
"parallel() requires functions, not promises: await parallel(items.map(item => () => agent(...))).",
|
|
24
|
+
].join(" "),
|
|
25
|
+
}),
|
|
26
|
+
args: Type.Optional(
|
|
27
|
+
Type.Any({ description: "Optional JSON value exposed to the workflow script as global `args`." }),
|
|
28
|
+
),
|
|
29
|
+
background: Type.Optional(
|
|
30
|
+
Type.Boolean({
|
|
31
|
+
description: "Run the workflow in the background. Default: false. When true, returns immediately with a run ID.",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
maxAgents: Type.Optional(
|
|
35
|
+
Type.Number({
|
|
36
|
+
description: "Maximum number of agents allowed in this run. Default: 1000.",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
agentTimeoutMs: Type.Optional(
|
|
40
|
+
Type.Number({
|
|
41
|
+
description: "Timeout per agent in milliseconds. Default: 300000 (5 minutes).",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type WorkflowToolInput = {
|
|
47
|
+
script: string;
|
|
48
|
+
args?: unknown;
|
|
49
|
+
background?: boolean;
|
|
50
|
+
maxAgents?: number;
|
|
51
|
+
agentTimeoutMs?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface WorkflowToolOptions {
|
|
55
|
+
cwd?: string;
|
|
56
|
+
concurrency?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createWorkflowTool(options: WorkflowToolOptions = {}): ToolDefinition<typeof workflowToolSchema, any> {
|
|
60
|
+
const manager = new WorkflowManager({ cwd: options.cwd, concurrency: options.concurrency });
|
|
61
|
+
const _storage = createWorkflowStorage(options.cwd ?? process.cwd());
|
|
62
|
+
|
|
63
|
+
return defineTool({
|
|
64
|
+
name: "workflow",
|
|
65
|
+
label: "Workflow",
|
|
66
|
+
description: [
|
|
67
|
+
"Execute a deterministic JavaScript workflow that orchestrates multiple subagents with agent(), parallel(), and pipeline().",
|
|
68
|
+
"script is required raw JavaScript. It must start with export const meta = { name, description, phases? } and must call agent() at least once.",
|
|
69
|
+
].join(" "),
|
|
70
|
+
promptSnippet:
|
|
71
|
+
"Run a deterministic JavaScript workflow. Required script header: export const meta = { name: 'short_snake_case', description: 'non-empty description', phases: [{ title: 'Phase' }] }.",
|
|
72
|
+
promptGuidelines: [
|
|
73
|
+
"Use workflow only when the user explicitly asks for a workflow, workflows, fan-out, or multi-agent orchestration.",
|
|
74
|
+
"For workflow, always pass one raw JavaScript string in the required script parameter; do not include Markdown fences or prose around the script.",
|
|
75
|
+
"For workflow, the script's first statement must be `export const meta = { name: 'short_snake_case', description: 'non-empty human description', phases: [{ title: 'Phase name' }] }`; meta.name and meta.description are required non-empty strings.",
|
|
76
|
+
"For workflow, write plain JavaScript after the meta export. Do not use TypeScript syntax, imports, require(), fs, Date.now(), Math.random(), or new Date().",
|
|
77
|
+
"For workflow, available globals are agent(prompt, opts), parallel(thunks), pipeline(items, ...stages), phase(title), log(message), args, cwd, process.cwd(), and budget. Every workflow must call agent() at least once; do not use workflow only to declare phases or return a static object.",
|
|
78
|
+
"For workflow, prefer it for decomposable work: repository inspection, independent research/checks, multi-perspective review, or fan-out/fan-in synthesis. Do not use it for a single quick file read/edit or when ordinary tools are enough.",
|
|
79
|
+
"For workflow, parallel() takes functions, not promises: use `await parallel(items.map(item => () => agent('...', { label: '...' })))`, never `await parallel(items.map(item => agent(...)))`. Results are returned in input order.",
|
|
80
|
+
"For workflow, pipeline(items, ...stages) runs each item through stages sequentially, while different items may run concurrently. Each stage receives (previousValue, originalItem, index).",
|
|
81
|
+
"For workflow, every agent() call should include a unique short label option, 2-5 words, such as { label: 'repo inventory' } or { label: 'source modules' }; unique labels make live status and error reporting readable.",
|
|
82
|
+
"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.",
|
|
83
|
+
"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.",
|
|
84
|
+
"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.",
|
|
85
|
+
"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.",
|
|
86
|
+
"For workflow, set background: true to run asynchronously. The workflow will return immediately with a run ID that can be used to check status later.",
|
|
87
|
+
],
|
|
88
|
+
parameters: workflowToolSchema,
|
|
89
|
+
prepareArguments(args) {
|
|
90
|
+
return normalizeWorkflowToolArgs(args);
|
|
91
|
+
},
|
|
92
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
93
|
+
const script = normalizeWorkflowScript(params.script);
|
|
94
|
+
const parsed = parseWorkflowScript(script);
|
|
95
|
+
|
|
96
|
+
// Background execution
|
|
97
|
+
if (params.background) {
|
|
98
|
+
const { runId } = manager.startInBackground(script, params.args);
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: [
|
|
104
|
+
`Workflow "${parsed.meta.name}" started in background.`,
|
|
105
|
+
`Run ID: ${runId}`,
|
|
106
|
+
`Use /workflow status ${runId} to check progress.`,
|
|
107
|
+
`Use /workflow stop ${runId} to cancel.`,
|
|
108
|
+
].join("\n"),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
details: { runId, background: true },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Synchronous execution (blocking)
|
|
116
|
+
let snapshot: WorkflowSnapshot = createWorkflowSnapshot(parsed.meta);
|
|
117
|
+
const display = createToolUpdateWorkflowDisplay(onUpdate, undefined, {
|
|
118
|
+
key: "workflow",
|
|
119
|
+
streamToolUpdates: true,
|
|
120
|
+
maxAgents: 4,
|
|
121
|
+
maxLogs: 1,
|
|
122
|
+
showResultPreviews: false,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const update = () => {
|
|
126
|
+
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
127
|
+
display.update(snapshot);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let result: WorkflowRunResult;
|
|
131
|
+
try {
|
|
132
|
+
result = await runWorkflow(script, {
|
|
133
|
+
cwd: options.cwd ?? ctx.cwd,
|
|
134
|
+
args: params.args,
|
|
135
|
+
signal,
|
|
136
|
+
concurrency: options.concurrency,
|
|
137
|
+
maxAgents: params.maxAgents,
|
|
138
|
+
agentTimeoutMs: params.agentTimeoutMs,
|
|
139
|
+
onLog(message) {
|
|
140
|
+
snapshot.logs.push(message);
|
|
141
|
+
update();
|
|
142
|
+
},
|
|
143
|
+
onPhase(title) {
|
|
144
|
+
snapshot.currentPhase = title;
|
|
145
|
+
if (!snapshot.phases.includes(title)) snapshot.phases.push(title);
|
|
146
|
+
update();
|
|
147
|
+
},
|
|
148
|
+
onAgentStart(event) {
|
|
149
|
+
if (signal?.aborted) throw new Error("Workflow was aborted");
|
|
150
|
+
snapshot.agents.push({
|
|
151
|
+
id: snapshot.agents.length + 1,
|
|
152
|
+
label: event.label,
|
|
153
|
+
phase: event.phase,
|
|
154
|
+
prompt: event.prompt,
|
|
155
|
+
status: "running",
|
|
156
|
+
});
|
|
157
|
+
update();
|
|
158
|
+
},
|
|
159
|
+
onAgentEnd(event) {
|
|
160
|
+
const agent = [...snapshot.agents]
|
|
161
|
+
.reverse()
|
|
162
|
+
.find((item) => item.label === event.label && item.status === "running");
|
|
163
|
+
if (agent) {
|
|
164
|
+
agent.status = event.result === null ? "error" : "done";
|
|
165
|
+
agent.resultPreview = preview(event.result);
|
|
166
|
+
agent.tokens = event.tokens;
|
|
167
|
+
}
|
|
168
|
+
update();
|
|
169
|
+
},
|
|
170
|
+
onTokenUsage(usage) {
|
|
171
|
+
snapshot.tokenUsage = usage;
|
|
172
|
+
update();
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (signal?.aborted || (error instanceof WorkflowError && error.code === WorkflowErrorCode.WORKFLOW_ABORTED)) {
|
|
177
|
+
for (const agent of snapshot.agents) {
|
|
178
|
+
if (agent.status === "running") {
|
|
179
|
+
agent.status = "skipped";
|
|
180
|
+
agent.error = "aborted";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
184
|
+
display.complete(snapshot);
|
|
185
|
+
throw new Error("Workflow was aborted");
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (result.agentCount === 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"workflow scripts must call agent() at least once; this workflow declared phases but did not run any subagents",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
snapshot.result = result.result;
|
|
197
|
+
snapshot.durationMs = result.durationMs;
|
|
198
|
+
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
199
|
+
display.complete(snapshot);
|
|
200
|
+
|
|
201
|
+
// Format token usage
|
|
202
|
+
const tokenInfo = result.tokenUsage ? `\n\nToken usage: ${result.tokenUsage.total.toLocaleString()} tokens` : "";
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Workflow ${result.meta.name} completed with ${result.agentCount} agent(s).\n\nResult:\n${JSON.stringify(result.result, null, 2)}${tokenInfo}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
details: {
|
|
212
|
+
...snapshot,
|
|
213
|
+
meta: result.meta,
|
|
214
|
+
phases: result.phases,
|
|
215
|
+
logs: result.logs,
|
|
216
|
+
result: result.result,
|
|
217
|
+
durationMs: result.durationMs,
|
|
218
|
+
tokenUsage: result.tokenUsage,
|
|
219
|
+
runId: result.runId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
renderCall(_args, theme) {
|
|
224
|
+
return new Text(theme.fg("toolTitle", theme.bold("workflow")), 0, 0);
|
|
225
|
+
},
|
|
226
|
+
renderResult(result, { isPartial }, theme) {
|
|
227
|
+
const snapshot = result.details as WorkflowSnapshot | undefined;
|
|
228
|
+
if (snapshot?.name) {
|
|
229
|
+
return new Text(renderWorkflowText(snapshot, !isPartial), 0, 0);
|
|
230
|
+
}
|
|
231
|
+
const text = result.content?.[0];
|
|
232
|
+
return new Text(text?.type === "text" ? text.text : theme.fg("muted", "workflow"), 0, 0);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeWorkflowToolArgs(args: unknown): WorkflowToolInput {
|
|
238
|
+
if (!args || typeof args !== "object") throw new Error("workflow requires an object argument with a script string");
|
|
239
|
+
const value = args as Record<string, unknown>;
|
|
240
|
+
if (typeof value.script !== "string") throw new Error("workflow requires `script` to be a string");
|
|
241
|
+
return { ...value, script: normalizeWorkflowScript(value.script) } as WorkflowToolInput;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeWorkflowScript(script: string): string {
|
|
245
|
+
let text = script.trim();
|
|
246
|
+
const fence = text.match(/^```(?:js|javascript)?\s*\n([\s\S]*?)\n```$/i);
|
|
247
|
+
if (fence) text = fence[1].trim();
|
|
248
|
+
return text;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _isAbortError(error: unknown): boolean {
|
|
252
|
+
if (!(error instanceof Error)) return false;
|
|
253
|
+
return /\babort(?:ed)?\b/i.test(error.message);
|
|
254
|
+
}
|