@baryonlabs/pi-dynamic-workflows 1.0.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/LICENSE +22 -0
- package/README.md +175 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +86 -0
- package/dist/display.d.ts +54 -0
- package/dist/display.js +169 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/workflow-tool.d.ts +16 -0
- package/dist/workflow-tool.js +183 -0
- package/dist/workflow.d.ts +53 -0
- package/dist/workflow.js +426 -0
- package/extensions/workflow.ts +14 -0
- package/package.json +71 -0
- package/src/agent.ts +131 -0
- package/src/display.ts +235 -0
- package/src/index.ts +30 -0
- package/src/structured-output.ts +47 -0
- package/src/workflow-tool.ts +209 -0
- package/src/workflow.ts +502 -0
- package/types/workflow.d.ts +95 -0
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import vm from "node:vm";
|
|
2
|
+
import type { Node } from "acorn";
|
|
3
|
+
import { parse } from "acorn";
|
|
4
|
+
import type { TSchema } from "typebox";
|
|
5
|
+
import { WorkflowAgent, type WorkflowAgentOptions } from "./agent.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowMetaPhase {
|
|
8
|
+
title: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkflowMeta {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
whenToUse?: string;
|
|
17
|
+
phases?: WorkflowMetaPhase[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
21
|
+
args?: unknown;
|
|
22
|
+
agent?: Pick<WorkflowAgent, "run">;
|
|
23
|
+
concurrency?: number;
|
|
24
|
+
tokenBudget?: number | null;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
onLog?: (message: string) => void;
|
|
27
|
+
onPhase?: (title: string) => void;
|
|
28
|
+
onAgentStart?: (event: { label: string; phase?: string; prompt: string }) => void;
|
|
29
|
+
onAgentEnd?: (event: { label: string; phase?: string; result: unknown }) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkflowRunResult<T = unknown> {
|
|
33
|
+
meta: WorkflowMeta;
|
|
34
|
+
result: T;
|
|
35
|
+
logs: string[];
|
|
36
|
+
phases: string[];
|
|
37
|
+
agentCount: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AgentOptions<TSchemaDef extends TSchema | undefined = TSchema | undefined> {
|
|
42
|
+
label?: string;
|
|
43
|
+
phase?: string;
|
|
44
|
+
schema?: TSchemaDef;
|
|
45
|
+
model?: string;
|
|
46
|
+
isolation?: "worktree";
|
|
47
|
+
agentType?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface RuntimeState {
|
|
51
|
+
currentPhase?: string;
|
|
52
|
+
logs: string[];
|
|
53
|
+
phases: string[];
|
|
54
|
+
agentCount: number;
|
|
55
|
+
spent: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type AnyNode = Node & { [key: string]: any; start: number; end: number };
|
|
59
|
+
|
|
60
|
+
const NONDETERMINISM_ERROR =
|
|
61
|
+
"Workflow scripts must be deterministic: Date.now()/Math.random()/new Date() are unavailable";
|
|
62
|
+
|
|
63
|
+
export async function runWorkflow<T = unknown>(
|
|
64
|
+
script: string,
|
|
65
|
+
options: WorkflowRunOptions = {},
|
|
66
|
+
): Promise<WorkflowRunResult<T>> {
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
const { meta, body } = parseWorkflowScript(script);
|
|
69
|
+
const state: RuntimeState = { logs: [], phases: [], agentCount: 0, spent: 0 };
|
|
70
|
+
const agentRunner = options.agent ?? new WorkflowAgent(options);
|
|
71
|
+
const concurrency = Math.max(
|
|
72
|
+
1,
|
|
73
|
+
Math.min(options.concurrency ?? Math.max(1, (globalThis.navigator?.hardwareConcurrency ?? 8) - 2), 16),
|
|
74
|
+
);
|
|
75
|
+
const limiter = createLimiter(concurrency);
|
|
76
|
+
const pendingAgentRuns = new Set<Promise<unknown>>();
|
|
77
|
+
|
|
78
|
+
const log = (message: string) => {
|
|
79
|
+
const text = String(message);
|
|
80
|
+
state.logs.push(text);
|
|
81
|
+
options.onLog?.(text);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const phase = (title: unknown) => {
|
|
85
|
+
const text = requireString(title, "phase title");
|
|
86
|
+
state.currentPhase = text;
|
|
87
|
+
if (!state.phases.includes(text)) state.phases.push(text);
|
|
88
|
+
options.onPhase?.(text);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const budget = Object.freeze({
|
|
92
|
+
total: options.tokenBudget ?? null,
|
|
93
|
+
spent: () => state.spent,
|
|
94
|
+
remaining: () => (options.tokenBudget == null ? Infinity : Math.max(0, options.tokenBudget - state.spent)),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const throwIfAborted = () => {
|
|
98
|
+
if (options.signal?.aborted) throw new Error("workflow aborted");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const agent = async (prompt: unknown, agentOptions: unknown = {}) => {
|
|
102
|
+
throwIfAborted();
|
|
103
|
+
if (budget.total !== null && budget.remaining() <= 0) throw new Error("workflow token budget exhausted");
|
|
104
|
+
const taskPrompt = requireString(prompt, "agent prompt");
|
|
105
|
+
const normalizedOptions = normalizeAgentOptions(agentOptions);
|
|
106
|
+
const assignedPhase = normalizedOptions.phase ?? state.currentPhase;
|
|
107
|
+
const requestedLabel = normalizedOptions.label?.trim();
|
|
108
|
+
const run = limiter(async () => {
|
|
109
|
+
state.agentCount++;
|
|
110
|
+
const label = requestedLabel || defaultAgentLabel(assignedPhase, state.agentCount);
|
|
111
|
+
options.onAgentStart?.({ label, phase: assignedPhase, prompt: taskPrompt });
|
|
112
|
+
try {
|
|
113
|
+
throwIfAborted();
|
|
114
|
+
const result = await agentRunner.run(taskPrompt, {
|
|
115
|
+
label,
|
|
116
|
+
schema: normalizedOptions.schema,
|
|
117
|
+
signal: options.signal,
|
|
118
|
+
instructions: buildAgentInstructions(assignedPhase, normalizedOptions),
|
|
119
|
+
} as any);
|
|
120
|
+
throwIfAborted();
|
|
121
|
+
state.spent += estimateTokens(result);
|
|
122
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result });
|
|
123
|
+
return result;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (options.signal?.aborted) throw error;
|
|
126
|
+
log(`agent ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result: null });
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
pendingAgentRuns.add(run);
|
|
132
|
+
run.then(
|
|
133
|
+
() => pendingAgentRuns.delete(run),
|
|
134
|
+
() => pendingAgentRuns.delete(run),
|
|
135
|
+
);
|
|
136
|
+
return run;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const parallel = async (thunks: Array<() => Promise<unknown>>) => {
|
|
140
|
+
throwIfAborted();
|
|
141
|
+
if (!Array.isArray(thunks)) throw new TypeError("parallel() expects an array of functions");
|
|
142
|
+
if (thunks.some((thunk) => typeof thunk !== "function")) {
|
|
143
|
+
throw new TypeError("parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)");
|
|
144
|
+
}
|
|
145
|
+
return Promise.all(
|
|
146
|
+
thunks.map(async (thunk, index) => {
|
|
147
|
+
try {
|
|
148
|
+
return await thunk();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (options.signal?.aborted) throw error;
|
|
151
|
+
log(`parallel[${index}] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const pipeline = async (
|
|
159
|
+
items: unknown[],
|
|
160
|
+
...stages: Array<(prev: unknown, original: unknown, index: number) => unknown>
|
|
161
|
+
) => {
|
|
162
|
+
throwIfAborted();
|
|
163
|
+
if (!Array.isArray(items)) throw new TypeError("pipeline() expects an array as the first argument");
|
|
164
|
+
if (stages.some((stage) => typeof stage !== "function")) {
|
|
165
|
+
throw new TypeError("pipeline() stages must be functions: pipeline(items, item => ..., result => ...)");
|
|
166
|
+
}
|
|
167
|
+
return Promise.all(
|
|
168
|
+
items.map(async (item, index) => {
|
|
169
|
+
let value: unknown = item;
|
|
170
|
+
for (const stage of stages) {
|
|
171
|
+
try {
|
|
172
|
+
throwIfAborted();
|
|
173
|
+
value = await stage(value, item, index);
|
|
174
|
+
throwIfAborted();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (options.signal?.aborted) throw error;
|
|
177
|
+
log(`pipeline[${index}] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Models sometimes hallucinate host APIs inside workflow scripts (bash, fetch,
|
|
187
|
+
// require, ...). A bare ReferenceError is opaque and, when thrown inside an
|
|
188
|
+
// un-awaited async call, escapes as an unhandled rejection. Predefine the
|
|
189
|
+
// usual suspects as guards that throw a descriptive, actionable error instead.
|
|
190
|
+
const unavailable = (name: string, hint: string) => () => {
|
|
191
|
+
throw new Error(`${name}() is not available in workflow scripts — ${hint}`);
|
|
192
|
+
};
|
|
193
|
+
const shellHint =
|
|
194
|
+
'delegate shell/file work to a subagent instead, e.g. await agent("run <command> and report the output")';
|
|
195
|
+
const guards = Object.fromEntries(
|
|
196
|
+
["bash", "sh", "shell", "exec", "execSync", "spawn", "spawnSync", "system"].map((name) => [
|
|
197
|
+
name,
|
|
198
|
+
unavailable(name, shellHint),
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
guards.fetch = unavailable("fetch", 'use await agent("fetch <url> and report ...") instead');
|
|
202
|
+
guards.require = unavailable("require", "workflow scripts have no module system; use agent()/parallel()/pipeline()");
|
|
203
|
+
|
|
204
|
+
const context = vm.createContext({
|
|
205
|
+
...guards,
|
|
206
|
+
agent,
|
|
207
|
+
parallel,
|
|
208
|
+
pipeline,
|
|
209
|
+
log,
|
|
210
|
+
phase,
|
|
211
|
+
args: options.args,
|
|
212
|
+
cwd: options.cwd ?? process.cwd(),
|
|
213
|
+
process: Object.freeze({ cwd: () => options.cwd ?? process.cwd() }),
|
|
214
|
+
budget,
|
|
215
|
+
console: {
|
|
216
|
+
log,
|
|
217
|
+
info: log,
|
|
218
|
+
warn: (m: unknown) => log(`[warn] ${String(m)}`),
|
|
219
|
+
error: (m: unknown) => log(`[error] ${String(m)}`),
|
|
220
|
+
},
|
|
221
|
+
JSON,
|
|
222
|
+
Math,
|
|
223
|
+
Array,
|
|
224
|
+
Object,
|
|
225
|
+
String,
|
|
226
|
+
Number,
|
|
227
|
+
Boolean,
|
|
228
|
+
Set,
|
|
229
|
+
Map,
|
|
230
|
+
Promise,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const wrapped = `(async () => {\n${body}\n})()`;
|
|
234
|
+
// A floating rejection inside the script (e.g. an un-awaited `main()` that
|
|
235
|
+
// throws) bypasses this await and escapes as a process-level unhandled
|
|
236
|
+
// rejection — killing the HOST process (pi exits, losing the session).
|
|
237
|
+
// Capture unhandled rejections for the duration of the run and fail the
|
|
238
|
+
// workflow with them instead, so the error comes back as a tool result.
|
|
239
|
+
// Node emits `unhandledRejection` at end-of-tick, possibly AFTER the script
|
|
240
|
+
// resolves — so keep listening one extra event-loop turn before deciding.
|
|
241
|
+
let floated = false;
|
|
242
|
+
let floatingReject!: (reason: unknown) => void;
|
|
243
|
+
const floatingRejection = new Promise<never>((_, reject) => {
|
|
244
|
+
floatingReject = (reason) => {
|
|
245
|
+
floated = true;
|
|
246
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
floatingRejection.catch(() => {}); // avoid the sentinel itself going unhandled
|
|
250
|
+
const onUnhandled = (reason: unknown) => floatingReject(reason);
|
|
251
|
+
process.on("unhandledRejection", onUnhandled);
|
|
252
|
+
let result: unknown;
|
|
253
|
+
try {
|
|
254
|
+
const scriptRun = new vm.Script(wrapped, { filename: `${meta.name || "workflow"}.js` }).runInContext(context);
|
|
255
|
+
result = await Promise.race([scriptRun, floatingRejection]);
|
|
256
|
+
await Promise.allSettled([...pendingAgentRuns]);
|
|
257
|
+
} finally {
|
|
258
|
+
// Late rejections surface on the next tick — absorb them before detaching,
|
|
259
|
+
// otherwise they'd escape to the process the instant we stop listening.
|
|
260
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
261
|
+
process.off("unhandledRejection", onUnhandled);
|
|
262
|
+
}
|
|
263
|
+
if (floated) await floatingRejection; // rethrow the captured floating error
|
|
264
|
+
assertStructuredCloneable(result, "workflow result");
|
|
265
|
+
return {
|
|
266
|
+
meta,
|
|
267
|
+
result: result as T,
|
|
268
|
+
logs: state.logs,
|
|
269
|
+
phases: state.phases,
|
|
270
|
+
agentCount: state.agentCount,
|
|
271
|
+
durationMs: Date.now() - started,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function parseWorkflowScript(script: string): { meta: WorkflowMeta; body: string } {
|
|
276
|
+
const ast = parse(script, {
|
|
277
|
+
ecmaVersion: "latest",
|
|
278
|
+
sourceType: "module",
|
|
279
|
+
allowAwaitOutsideFunction: true,
|
|
280
|
+
allowReturnOutsideFunction: true,
|
|
281
|
+
ranges: false,
|
|
282
|
+
}) as AnyNode;
|
|
283
|
+
|
|
284
|
+
assertDeterministicAst(ast);
|
|
285
|
+
|
|
286
|
+
const first = ast.body?.[0] as AnyNode | undefined;
|
|
287
|
+
if (first?.type !== "ExportNamedDeclaration") {
|
|
288
|
+
throw new Error("`export const meta = { name, description }` must be the first statement in the script");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const declaration = first.declaration as AnyNode | null;
|
|
292
|
+
if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
|
|
293
|
+
throw new Error("meta export must be `export const meta = ...`");
|
|
294
|
+
}
|
|
295
|
+
if (declaration.declarations.length !== 1) {
|
|
296
|
+
throw new Error("meta export must declare only `meta`");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const declarator = declaration.declarations[0] as AnyNode;
|
|
300
|
+
if (declarator.id?.type !== "Identifier" || declarator.id.name !== "meta") {
|
|
301
|
+
throw new Error("meta export must declare `meta`");
|
|
302
|
+
}
|
|
303
|
+
if (!declarator.init) throw new Error("meta must have a literal value");
|
|
304
|
+
|
|
305
|
+
const meta = evaluateLiteral(declarator.init, "meta");
|
|
306
|
+
validateMeta(meta);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
meta,
|
|
310
|
+
body: script.slice(0, first.start) + script.slice(first.end),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function evaluateLiteral(node: AnyNode, path: string): unknown {
|
|
315
|
+
switch (node.type) {
|
|
316
|
+
case "ObjectExpression": {
|
|
317
|
+
const out: Record<string, unknown> = {};
|
|
318
|
+
for (const prop of node.properties as AnyNode[]) {
|
|
319
|
+
if (prop.type === "SpreadElement") throw new Error(`spread not allowed in ${path}`);
|
|
320
|
+
if (prop.type !== "Property") throw new Error(`only plain properties allowed in ${path}`);
|
|
321
|
+
if (prop.computed) throw new Error(`computed keys not allowed in ${path}`);
|
|
322
|
+
if (prop.kind !== "init" || prop.method) throw new Error(`methods/accessors not allowed in ${path}`);
|
|
323
|
+
const key = propertyKey(prop.key as AnyNode, path);
|
|
324
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
325
|
+
throw new Error(`reserved key name not allowed in ${path}: ${key}`);
|
|
326
|
+
}
|
|
327
|
+
out[key] = evaluateLiteral(prop.value as AnyNode, `${path}.${key}`);
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
case "ArrayExpression":
|
|
332
|
+
return (node.elements as Array<AnyNode | null>).map((element, index) => {
|
|
333
|
+
if (!element) throw new Error(`sparse arrays not allowed in ${path}`);
|
|
334
|
+
if (element.type === "SpreadElement") throw new Error(`spread not allowed in ${path}`);
|
|
335
|
+
return evaluateLiteral(element, `${path}[${index}]`);
|
|
336
|
+
});
|
|
337
|
+
case "Literal":
|
|
338
|
+
return node.value;
|
|
339
|
+
case "TemplateLiteral":
|
|
340
|
+
if (node.expressions.length > 0) throw new Error(`template interpolation not allowed in ${path}`);
|
|
341
|
+
return node.quasis.map((quasi: AnyNode) => quasi.value.cooked ?? quasi.value.raw).join("");
|
|
342
|
+
case "UnaryExpression":
|
|
343
|
+
if (node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number") {
|
|
344
|
+
return -node.argument.value;
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`only negative-number unary allowed in ${path}`);
|
|
347
|
+
default:
|
|
348
|
+
throw new Error(`non-literal node type in ${path}: ${node.type}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function propertyKey(node: AnyNode, path: string): string {
|
|
353
|
+
if (node.type === "Identifier") return node.name;
|
|
354
|
+
if (node.type === "Literal" && (typeof node.value === "string" || typeof node.value === "number"))
|
|
355
|
+
return String(node.value);
|
|
356
|
+
throw new Error(`unsupported key type in ${path}: ${node.type}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function assertDeterministicAst(node: AnyNode): void {
|
|
360
|
+
if (isDateNowCall(node) || isMathRandomCall(node) || isNewDateExpression(node)) {
|
|
361
|
+
throw new Error(NONDETERMINISM_ERROR);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const child of astChildren(node)) assertDeterministicAst(child);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function astChildren(node: AnyNode): AnyNode[] {
|
|
368
|
+
const children: AnyNode[] = [];
|
|
369
|
+
for (const value of Object.values(node)) {
|
|
370
|
+
if (Array.isArray(value)) children.push(...value.filter(isAstNode));
|
|
371
|
+
else if (isAstNode(value)) children.push(value);
|
|
372
|
+
}
|
|
373
|
+
return children;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isAstNode(value: unknown): value is AnyNode {
|
|
377
|
+
return !!value && typeof value === "object" && typeof (value as AnyNode).type === "string";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isDateNowCall(node: AnyNode): boolean {
|
|
381
|
+
return node.type === "CallExpression" && isMemberExpression(node.callee, "Date", "now");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isMathRandomCall(node: AnyNode): boolean {
|
|
385
|
+
return node.type === "CallExpression" && isMemberExpression(node.callee, "Math", "random");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isNewDateExpression(node: AnyNode): boolean {
|
|
389
|
+
return node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Date";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isMemberExpression(node: AnyNode | undefined, objectName: string, propertyName: string): boolean {
|
|
393
|
+
if (node?.type !== "MemberExpression" || node.object?.type !== "Identifier" || node.object.name !== objectName) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
return propertyNameOf(node) === propertyName;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function propertyNameOf(node: AnyNode): string | undefined {
|
|
400
|
+
if (!node.computed && node.property?.type === "Identifier") return node.property.name;
|
|
401
|
+
return staticStringOf(node.property);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function staticStringOf(node: AnyNode | undefined): string | undefined {
|
|
405
|
+
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
406
|
+
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
407
|
+
return node.quasis.map((quasi: AnyNode) => quasi.value.cooked ?? quasi.value.raw).join("");
|
|
408
|
+
}
|
|
409
|
+
if (node?.type === "BinaryExpression" && node.operator === "+") {
|
|
410
|
+
const left = staticStringOf(node.left);
|
|
411
|
+
const right = staticStringOf(node.right);
|
|
412
|
+
if (left !== undefined && right !== undefined) return left + right;
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
|
|
418
|
+
if (!meta || typeof meta !== "object") throw new Error("meta must be an object");
|
|
419
|
+
const value = meta as WorkflowMeta;
|
|
420
|
+
if (typeof value.name !== "string" || !value.name.trim()) throw new Error("meta.name must be a non-empty string");
|
|
421
|
+
if (typeof value.description !== "string" || !value.description.trim())
|
|
422
|
+
throw new Error("meta.description must be a non-empty string");
|
|
423
|
+
if (value.whenToUse !== undefined && typeof value.whenToUse !== "string")
|
|
424
|
+
throw new Error("meta.whenToUse must be a string");
|
|
425
|
+
if (value.phases !== undefined) {
|
|
426
|
+
if (!Array.isArray(value.phases)) throw new Error("meta.phases must be an array");
|
|
427
|
+
for (const phase of value.phases) {
|
|
428
|
+
if (!phase || typeof phase !== "object" || typeof (phase as WorkflowMetaPhase).title !== "string") {
|
|
429
|
+
throw new Error("each meta phase must have a title string");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function createLimiter(limit: number) {
|
|
436
|
+
let active = 0;
|
|
437
|
+
const queue: Array<() => void> = [];
|
|
438
|
+
const next = () => {
|
|
439
|
+
active--;
|
|
440
|
+
queue.shift()?.();
|
|
441
|
+
};
|
|
442
|
+
return async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
443
|
+
if (active >= limit) await new Promise<void>((resolve) => queue.push(resolve));
|
|
444
|
+
active++;
|
|
445
|
+
try {
|
|
446
|
+
return await fn();
|
|
447
|
+
} finally {
|
|
448
|
+
next();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function requireString(value: unknown, name: string): string {
|
|
454
|
+
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function optionalString(value: unknown, name: string): string | undefined {
|
|
459
|
+
if (value === undefined) return undefined;
|
|
460
|
+
return requireString(value, name);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function normalizeAgentOptions(value: unknown): AgentOptions {
|
|
464
|
+
if (!value || typeof value !== "object") throw new TypeError("agent options must be an object");
|
|
465
|
+
const options = value as AgentOptions;
|
|
466
|
+
return {
|
|
467
|
+
...options,
|
|
468
|
+
label: optionalString(options.label, "agent label"),
|
|
469
|
+
phase: optionalString(options.phase, "agent phase"),
|
|
470
|
+
model: optionalString(options.model, "agent model"),
|
|
471
|
+
isolation: options.isolation,
|
|
472
|
+
agentType: optionalString(options.agentType, "agent type"),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function assertStructuredCloneable(value: unknown, name: string): void {
|
|
477
|
+
try {
|
|
478
|
+
structuredClone(value);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
481
|
+
throw new Error(
|
|
482
|
+
`${name} must be structured-cloneable; did you forget to await agent(), parallel(), or pipeline()?${detail}`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function defaultAgentLabel(phase: string | undefined, index: number): string {
|
|
488
|
+
return phase ? `${phase} agent ${index}` : `agent ${index}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildAgentInstructions(phase: string | undefined, options: AgentOptions): string | undefined {
|
|
492
|
+
const lines = [];
|
|
493
|
+
if (phase) lines.push(`Workflow phase: ${phase}`);
|
|
494
|
+
if (options.agentType) lines.push(`Act as workflow subagent type: ${options.agentType}`);
|
|
495
|
+
if (options.isolation) lines.push(`Requested isolation: ${options.isolation}`);
|
|
496
|
+
if (options.model) lines.push(`Requested model: ${options.model}`);
|
|
497
|
+
return lines.length ? lines.join("\n") : undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function estimateTokens(value: unknown): number {
|
|
501
|
+
return Math.ceil(JSON.stringify(value ?? "").length / 4);
|
|
502
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient globals available inside pi-dynamic-workflows workflow scripts.
|
|
3
|
+
*
|
|
4
|
+
* Add this to a JavaScript or TypeScript workflow file for editor IntelliSense:
|
|
5
|
+
*
|
|
6
|
+
* /// <reference types="pi-dynamic-workflows/workflow" />
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {};
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
/** Literal workflow metadata. Must be the first statement: `export const meta = { ... }`. */
|
|
13
|
+
interface WorkflowMeta {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
whenToUse?: string;
|
|
17
|
+
/** Optional documentation for an expected outline. Live progress is driven by `phase(...)`. */
|
|
18
|
+
phases?: WorkflowMetaPhase[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WorkflowMetaPhase {
|
|
22
|
+
title: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface WorkflowAgentOptions<TSchema = JsonSchema> {
|
|
28
|
+
/** Short label shown in the live progress UI. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Override the current runtime phase for this agent. */
|
|
31
|
+
phase?: string;
|
|
32
|
+
/** JSON Schema for structured output. When present, the returned value is typed as unknown unless you provide a generic. */
|
|
33
|
+
schema?: TSchema;
|
|
34
|
+
/** Requested model name. Currently passed as subagent guidance. */
|
|
35
|
+
model?: string;
|
|
36
|
+
/** Requested isolation mode. */
|
|
37
|
+
isolation?: "worktree";
|
|
38
|
+
/** Requested subagent role/type. */
|
|
39
|
+
agentType?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
43
|
+
type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
44
|
+
interface JsonObject {
|
|
45
|
+
[key: string]: JsonValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface JsonSchema {
|
|
49
|
+
type?: string | string[];
|
|
50
|
+
properties?: Record<string, JsonSchema>;
|
|
51
|
+
items?: JsonSchema | JsonSchema[];
|
|
52
|
+
required?: string[];
|
|
53
|
+
additionalProperties?: boolean | JsonSchema;
|
|
54
|
+
enum?: JsonValue[];
|
|
55
|
+
const?: JsonValue;
|
|
56
|
+
description?: string;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface WorkflowBudget {
|
|
61
|
+
total: number | null;
|
|
62
|
+
spent(): number;
|
|
63
|
+
remaining(): number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Spawn a subagent. Returns final text unless a structured-output schema is used with an explicit generic. */
|
|
67
|
+
function agent<T = string>(prompt: string, options?: WorkflowAgentOptions): Promise<T>;
|
|
68
|
+
|
|
69
|
+
/** Run independent async tasks concurrently. Pass functions, not already-created promises. */
|
|
70
|
+
function parallel<T>(thunks: Array<() => Promise<T>>): Promise<T[]>;
|
|
71
|
+
|
|
72
|
+
/** Run each item through sequential async stages while different items may run concurrently. */
|
|
73
|
+
function pipeline<TItem, TResult = unknown>(
|
|
74
|
+
items: TItem[],
|
|
75
|
+
...stages: Array<(previous: unknown, original: TItem, index: number) => TResult | Promise<TResult>>
|
|
76
|
+
): Promise<TResult[]>;
|
|
77
|
+
|
|
78
|
+
/** Mark the current workflow phase for progress grouping. */
|
|
79
|
+
function phase(title: string): void;
|
|
80
|
+
|
|
81
|
+
/** Append a workflow-level log line. */
|
|
82
|
+
function log(message: unknown): void;
|
|
83
|
+
|
|
84
|
+
/** Optional JSON args passed to the workflow tool. Narrow with a local type assertion when needed. */
|
|
85
|
+
const args: unknown;
|
|
86
|
+
|
|
87
|
+
/** Current working directory for the workflow/subagents. */
|
|
88
|
+
const cwd: string;
|
|
89
|
+
|
|
90
|
+
/** Deterministic process shim exposing only cwd(). */
|
|
91
|
+
const process: { cwd(): string };
|
|
92
|
+
|
|
93
|
+
/** Simple token-budget estimate for workflow runs. */
|
|
94
|
+
const budget: WorkflowBudget;
|
|
95
|
+
}
|