pi-workflow-engine 0.4.0 → 0.5.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/.pi/extensions/pi-workflow-engine/index.ts +127 -14
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +2 -33
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +396 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +57 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -3
- package/README.md +38 -11
- package/USAGE.md +183 -0
- package/package.json +2 -1
- package/.pi/extensions/pi-workflow-engine/workflows/ping.ts +0 -29
|
@@ -4,6 +4,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
|
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import type { WorkflowModule, WorkflowRunOptions } from "./src/types.ts";
|
|
6
6
|
import type { PerfSink, PerfSnapshot } from "./src/perf.ts";
|
|
7
|
+
import { registerDynamax } from "./src/dynamax.ts";
|
|
7
8
|
import { isWorkflowResult, renderWorkflowResult, type WorkflowPerfDetails, type WorkflowResultEnvelope } from "./src/ui/workflow-result-renderer.ts";
|
|
8
9
|
|
|
9
10
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
@@ -38,6 +39,7 @@ function formatPerfLine(perf: WorkflowPerfDetails | undefined): string | undefin
|
|
|
38
39
|
|
|
39
40
|
type DiscoveryModule = typeof import("./src/discovery.ts");
|
|
40
41
|
type EngineModule = typeof import("./src/engine.ts");
|
|
42
|
+
type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
|
|
41
43
|
|
|
42
44
|
async function loadDiscovery(): Promise<DiscoveryModule> {
|
|
43
45
|
return await import("./src/discovery.ts");
|
|
@@ -47,6 +49,10 @@ async function loadEngine(): Promise<EngineModule> {
|
|
|
47
49
|
return await import("./src/engine.ts");
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
|
|
53
|
+
return await import("./src/inline-workflow.ts");
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSink | undefined> {
|
|
51
57
|
const enabled = options.perf ?? process.env.PI_WORKFLOW_PERF === "1";
|
|
52
58
|
if (!enabled) return undefined;
|
|
@@ -54,11 +60,14 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
|
|
|
54
60
|
return createPerfRecorder(true);
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
const AUTHOR_TEMP_WORKFLOW_CHOICE = "✍ Author temporary one-shot workflow…";
|
|
64
|
+
|
|
57
65
|
export interface WorkflowInvocation {
|
|
58
66
|
name: string;
|
|
59
67
|
args: string;
|
|
60
68
|
options: WorkflowRunOptions;
|
|
61
69
|
refreshDiscovery?: boolean;
|
|
70
|
+
authorBrief?: string;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
@@ -120,14 +129,77 @@ function parseNumericOption(value: string | undefined): number | undefined {
|
|
|
120
129
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
function compactInlinePreview(script: string | undefined): string {
|
|
133
|
+
if (!script) return "";
|
|
134
|
+
const compact = script.replace(/\s+/g, " ").trim();
|
|
135
|
+
return compact.length > 60 ? `${compact.slice(0, 57)}…` : compact;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
|
|
139
|
+
return `dynamax author and run a temporary one-shot inline workflow.
|
|
140
|
+
|
|
141
|
+
User brief:
|
|
142
|
+
${brief.trim()}
|
|
143
|
+
|
|
144
|
+
Use the workflow tool with a script argument, not a saved workflow name.
|
|
145
|
+
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
146
|
+
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
147
|
+
Set thinkingLevel explicitly on each agent() call.
|
|
148
|
+
Do not edit files unless the user explicitly requested edits.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface WorkflowToolRequestParams {
|
|
152
|
+
readonly name?: string;
|
|
153
|
+
readonly script?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type WorkflowToolRequest =
|
|
157
|
+
| { readonly kind: "named"; readonly name: string }
|
|
158
|
+
| { readonly kind: "inline"; readonly script: string }
|
|
159
|
+
| { readonly kind: "error"; readonly error: "invalid_workflow_invocation"; readonly message: string };
|
|
160
|
+
|
|
161
|
+
export interface WorkflowToolErrorResult {
|
|
162
|
+
readonly content: Array<{ readonly type: "text"; readonly text: string }>;
|
|
163
|
+
readonly details: { readonly error: "invalid_workflow_invocation" } | { readonly error: "inline_compile_error"; readonly message: string };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const INVALID_WORKFLOW_INVOCATION_MESSAGE = "Provide exactly one workflow name or inline workflow script.";
|
|
167
|
+
|
|
168
|
+
export function normalizeWorkflowToolRequest(params: WorkflowToolRequestParams): WorkflowToolRequest {
|
|
169
|
+
const name = params.name?.trim() ?? "";
|
|
170
|
+
const script = params.script?.trim() ?? "";
|
|
171
|
+
const hasName = name.length > 0;
|
|
172
|
+
const hasScript = script.length > 0;
|
|
173
|
+
if (hasName === hasScript) return { kind: "error", error: "invalid_workflow_invocation", message: INVALID_WORKFLOW_INVOCATION_MESSAGE };
|
|
174
|
+
return hasName ? { kind: "named", name } : { kind: "inline", script };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function invalidWorkflowInvocationResult(): WorkflowToolErrorResult {
|
|
178
|
+
return { content: [{ type: "text", text: INVALID_WORKFLOW_INVOCATION_MESSAGE }], details: { error: "invalid_workflow_invocation" } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function inlineCompileErrorResult(message: string): WorkflowToolErrorResult {
|
|
182
|
+
return { content: [{ type: "text", text: `Inline workflow did not compile: ${message}` }], details: { error: "inline_compile_error", message } };
|
|
183
|
+
}
|
|
184
|
+
|
|
123
185
|
async function pickWorkflow(
|
|
124
186
|
workflows: ReadonlyMap<string, WorkflowModule>,
|
|
125
187
|
ctx: ExtensionCommandContext,
|
|
126
188
|
): Promise<WorkflowInvocation | undefined> {
|
|
127
|
-
const choices = [...workflows.values()].map((mod) => `${mod.meta.name} — ${mod.meta.description}`);
|
|
189
|
+
const choices = [AUTHOR_TEMP_WORKFLOW_CHOICE, ...[...workflows.values()].map((mod) => `${mod.meta.name} — ${mod.meta.description}`)];
|
|
128
190
|
const choice = await ctx.ui.select("Run workflow", choices);
|
|
129
191
|
if (!choice) return undefined;
|
|
130
192
|
|
|
193
|
+
if (choice === AUTHOR_TEMP_WORKFLOW_CHOICE) {
|
|
194
|
+
const brief = await ctx.ui.editor(
|
|
195
|
+
"Describe temporary workflow",
|
|
196
|
+
"Goal:\n\nAgents to run:\n- \n\nFinal output should include:\n- summary\n- findings\n- next steps\n",
|
|
197
|
+
);
|
|
198
|
+
const trimmed = brief?.trim();
|
|
199
|
+
if (!trimmed) return undefined;
|
|
200
|
+
return { name: "", args: "", options: {}, authorBrief: trimmed };
|
|
201
|
+
}
|
|
202
|
+
|
|
131
203
|
const separator = choice.indexOf(" — ");
|
|
132
204
|
const name = separator === -1 ? choice : choice.slice(0, separator);
|
|
133
205
|
const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
|
|
@@ -163,6 +235,8 @@ async function sendWorkflowResult(
|
|
|
163
235
|
}
|
|
164
236
|
|
|
165
237
|
export default function workflowEngine(pi: ExtensionAPI): void {
|
|
238
|
+
registerDynamax(pi);
|
|
239
|
+
|
|
166
240
|
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
167
241
|
const details = message.details;
|
|
168
242
|
if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
|
|
@@ -185,6 +259,11 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
185
259
|
return;
|
|
186
260
|
}
|
|
187
261
|
|
|
262
|
+
if (invocation.authorBrief) {
|
|
263
|
+
pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(invocation.authorBrief));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
188
267
|
const mod = workflows.get(invocation.name);
|
|
189
268
|
if (!mod) {
|
|
190
269
|
ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
|
|
@@ -200,9 +279,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
200
279
|
name: "workflow",
|
|
201
280
|
label: "Workflow",
|
|
202
281
|
description:
|
|
203
|
-
"
|
|
282
|
+
"ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize) and returns its structured result.",
|
|
283
|
+
promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
|
|
284
|
+
promptGuidelines: [
|
|
285
|
+
"Use workflow only when the user opted into workflow orchestration via `dynamax`, `/dynamax on`, an explicit request to run/author a workflow, or a command/skill instruction.",
|
|
286
|
+
"Use workflow with `name` for existing registered workflows such as code-review, diagnose, refactor-scout, or perf-review.",
|
|
287
|
+
"Use workflow with `script` for a new one-off inline workflow; the script must start with `export const meta = { ... }` and default-export an async workflow function.",
|
|
288
|
+
"Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
|
|
289
|
+
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
290
|
+
],
|
|
204
291
|
parameters: Type.Object({
|
|
205
|
-
name: Type.String({ description: "Workflow name, e.g. code-review" }),
|
|
292
|
+
name: Type.Optional(Type.String({ description: "Workflow name, e.g. code-review. Provide exactly one of name or script." })),
|
|
293
|
+
script: Type.Optional(Type.String({ description: "Inline workflow script. Provide exactly one of script or name." })),
|
|
206
294
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
207
295
|
concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
|
|
208
296
|
parallelSubmissionLimit: Type.Optional(Type.Number({ description: "Optional limit for eagerly submitted parallel thunks" })),
|
|
@@ -210,7 +298,12 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
210
298
|
}),
|
|
211
299
|
renderCall(args, theme) {
|
|
212
300
|
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
213
|
-
|
|
301
|
+
if (args.name?.trim()) {
|
|
302
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${suffix}`, 0, 0);
|
|
303
|
+
}
|
|
304
|
+
const preview = compactInlinePreview(args.script);
|
|
305
|
+
const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
|
|
306
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${suffix}${previewSuffix}`, 0, 0);
|
|
214
307
|
},
|
|
215
308
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
216
309
|
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
@@ -221,6 +314,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
221
314
|
return new Text(theme.fg("muted", text), 0, 0);
|
|
222
315
|
},
|
|
223
316
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
317
|
+
const request = normalizeWorkflowToolRequest(params);
|
|
318
|
+
if (request.kind === "error") return invalidWorkflowInvocationResult();
|
|
319
|
+
|
|
224
320
|
const runOptions: WorkflowRunOptions = {
|
|
225
321
|
concurrency: params.concurrency,
|
|
226
322
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
@@ -228,16 +324,33 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
228
324
|
signal,
|
|
229
325
|
};
|
|
230
326
|
const perfRecorder = await createInvocationPerf(runOptions);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
327
|
+
let mod: WorkflowModule;
|
|
328
|
+
let resultName: string;
|
|
329
|
+
|
|
330
|
+
if (request.kind === "named") {
|
|
331
|
+
const { discoverWorkflows } = await loadDiscovery();
|
|
332
|
+
const workflows = await discoverWorkflows(EXTENSION_DIR, { perf: perfRecorder });
|
|
333
|
+
const named = workflows.get(request.name);
|
|
334
|
+
if (!named) {
|
|
335
|
+
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: "text", text: `Unknown workflow "${request.name}". Available: ${available}` }],
|
|
338
|
+
details: { error: "unknown_workflow", available },
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
mod = named;
|
|
342
|
+
resultName = request.name;
|
|
343
|
+
} else {
|
|
344
|
+
const inline = await loadInlineWorkflow();
|
|
345
|
+
try {
|
|
346
|
+
mod = inline.compileInlineWorkflow(request.script);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof inline.InlineWorkflowCompileError) return inlineCompileErrorResult(error.message);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
resultName = mod.meta.name;
|
|
240
352
|
}
|
|
353
|
+
|
|
241
354
|
const { runWorkflow } = await loadEngine();
|
|
242
355
|
let perfSnapshot: PerfSnapshot | undefined;
|
|
243
356
|
const result = await runWorkflow(ctx, mod, params.args ?? "", {
|
|
@@ -250,7 +363,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
250
363
|
});
|
|
251
364
|
const perf = compactPerfSnapshot(perfSnapshot);
|
|
252
365
|
const perfLine = formatPerfLine(perf);
|
|
253
|
-
return { content: [{ type: "text", text: `${summarize(result)}${perfLine ? `\n\n${perfLine}` : ""}` }], details: workflowEnvelope(
|
|
366
|
+
return { content: [{ type: "text", text: `${summarize(result)}${perfLine ? `\n\n${perfLine}` : ""}` }], details: workflowEnvelope(resultName, result, perf) };
|
|
254
367
|
},
|
|
255
368
|
});
|
|
256
369
|
}
|
|
@@ -2,15 +2,11 @@ import { readdir } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import type {
|
|
5
|
+
import type { WorkflowModule } from "./types.ts";
|
|
6
6
|
import type { PerfSink } from "./perf.ts";
|
|
7
|
+
import { parseWorkflowModule } from "./workflow-module.ts";
|
|
7
8
|
import { BUILTIN_WORKFLOW_FILES, BUILTIN_WORKFLOWS } from "./workflows.ts";
|
|
8
9
|
|
|
9
|
-
type WorkflowModuleCandidate = {
|
|
10
|
-
meta?: { name?: unknown; description?: unknown; phases?: unknown };
|
|
11
|
-
default?: unknown;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
10
|
export interface DiscoverWorkflowsOptions {
|
|
15
11
|
readonly refresh?: boolean;
|
|
16
12
|
readonly perf?: PerfSink;
|
|
@@ -19,33 +15,6 @@ export interface DiscoverWorkflowsOptions {
|
|
|
19
15
|
|
|
20
16
|
const discoveryCache = new Map<string, Map<string, WorkflowModule>>();
|
|
21
17
|
|
|
22
|
-
function parseWorkflowModule(value: unknown): { module: WorkflowModule } | { reason: string } {
|
|
23
|
-
if (!value || typeof value !== "object") return { reason: "module export is not an object" };
|
|
24
|
-
const candidate = value as WorkflowModuleCandidate;
|
|
25
|
-
const meta = candidate.meta;
|
|
26
|
-
if (!meta || typeof meta !== "object") return { reason: "missing meta export" };
|
|
27
|
-
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
28
|
-
|
|
29
|
-
const description = meta.description;
|
|
30
|
-
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
31
|
-
|
|
32
|
-
const phases = meta.phases;
|
|
33
|
-
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
34
|
-
if (!isWorkflowRun(candidate.default)) return { reason: "default export must be a function" };
|
|
35
|
-
|
|
36
|
-
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
37
|
-
if (phases !== undefined) workflowMeta.phases = phases;
|
|
38
|
-
return { module: { meta: workflowMeta, default: candidate.default } };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
42
|
-
return typeof value === "function";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isWorkflowPhases(value: unknown): value is Array<{ title: string }> {
|
|
46
|
-
return Array.isArray(value) && value.every((phase) => typeof phase === "object" && phase !== null && typeof (phase as { title?: unknown }).title === "string");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
18
|
/** Best-effort dynamic load of every `*.ts` workflow in a directory. */
|
|
50
19
|
async function loadDir(dir: string, excludeFiles: ReadonlySet<string> = new Set()): Promise<WorkflowModule[]> {
|
|
51
20
|
let entries: string[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export interface DynamaxState {
|
|
5
|
+
sticky: boolean;
|
|
6
|
+
oneShotPending: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
|
|
10
|
+
|
|
11
|
+
export const DYNAMAX_REMINDER = `
|
|
12
|
+
## dynamax workflow opt-in
|
|
13
|
+
|
|
14
|
+
The user has opted into dynamax multi-agent orchestration. The workflow tool is permitted for this task. You may either run an existing named workflow or author a new inline workflow script when that best serves the user's request.
|
|
15
|
+
|
|
16
|
+
Inline workflow rules:
|
|
17
|
+
- Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
|
|
18
|
+
- Do not use import statements or dynamic import() in inline workflow scripts.
|
|
19
|
+
- Set thinkingLevel explicitly on each agent() call so fan-out remains bounded.
|
|
20
|
+
- Provide exactly one of workflow.name or workflow.script.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const DYNAMAX_CONTEXT_CUSTOM_TYPE = "workflow-dynamax-reminder";
|
|
24
|
+
|
|
25
|
+
export function createDynamaxState(): DynamaxState {
|
|
26
|
+
return { sticky: false, oneShotPending: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasDynamaxToken(text: string): boolean {
|
|
30
|
+
return DYNAMAX_TOKEN_PATTERN.test(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function markDynamaxOneShot(state: DynamaxState): void {
|
|
34
|
+
state.oneShotPending = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setDynamaxSticky(state: DynamaxState, sticky: boolean): void {
|
|
38
|
+
state.sticky = sticky;
|
|
39
|
+
if (!sticky) state.oneShotPending = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function clearDynamax(state: DynamaxState): void {
|
|
43
|
+
state.sticky = false;
|
|
44
|
+
state.oneShotPending = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function consumeDynamaxOneShot(state: DynamaxState): boolean {
|
|
48
|
+
const pending = state.oneShotPending;
|
|
49
|
+
state.oneShotPending = false;
|
|
50
|
+
return pending;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isDynamaxActive(state: DynamaxState): boolean {
|
|
54
|
+
return state.sticky || state.oneShotPending;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function appendDynamaxSystemReminder(systemPrompt: string, state: DynamaxState): string {
|
|
58
|
+
if (!isDynamaxActive(state)) return systemPrompt;
|
|
59
|
+
state.oneShotPending = false;
|
|
60
|
+
return `${systemPrompt}\n\n${DYNAMAX_REMINDER.trim()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function appendDynamaxContextReminder(messages: AgentMessage[], state: DynamaxState): AgentMessage[] {
|
|
64
|
+
if (!state.sticky) return messages;
|
|
65
|
+
return [...messages, createDynamaxContextMessage()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerDynamax(pi: ExtensionAPI): void {
|
|
69
|
+
const state = createDynamaxState();
|
|
70
|
+
|
|
71
|
+
pi.on("input", (event) => {
|
|
72
|
+
if (event.source === "extension") return { action: "continue" };
|
|
73
|
+
if (hasDynamaxToken(event.text)) markDynamaxOneShot(state);
|
|
74
|
+
return { action: "continue" };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("before_agent_start", (event) => {
|
|
78
|
+
const systemPrompt = appendDynamaxSystemReminder(event.systemPrompt, state);
|
|
79
|
+
if (systemPrompt === event.systemPrompt) return undefined;
|
|
80
|
+
return { systemPrompt };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("context", (event) => {
|
|
84
|
+
const messages = appendDynamaxContextReminder(event.messages, state);
|
|
85
|
+
if (messages === event.messages) return undefined;
|
|
86
|
+
return { messages };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
pi.registerCommand("dynamax", {
|
|
90
|
+
description: "Toggle dynamax workflow orchestration opt-in: /dynamax on|off|status",
|
|
91
|
+
handler: async (args, ctx) => {
|
|
92
|
+
const action = args.trim().toLowerCase();
|
|
93
|
+
if (action === "on") {
|
|
94
|
+
setDynamaxSticky(state, true);
|
|
95
|
+
ctx.ui.notify("dynamax workflow orchestration is on for this session", "info");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (action === "off") {
|
|
99
|
+
clearDynamax(state);
|
|
100
|
+
ctx.ui.notify("dynamax workflow orchestration is off", "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (action === "status") {
|
|
104
|
+
const sticky = state.sticky ? "on" : "off";
|
|
105
|
+
const oneShot = state.oneShotPending ? "; one-shot opt-in pending" : "";
|
|
106
|
+
ctx.ui.notify(`dynamax sticky mode is ${sticky}${oneShot}`, "info");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
ctx.ui.notify("Usage: /dynamax on|off|status", "warning");
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createDynamaxContextMessage(): AgentMessage {
|
|
115
|
+
return {
|
|
116
|
+
role: "custom",
|
|
117
|
+
customType: DYNAMAX_CONTEXT_CUSTOM_TYPE,
|
|
118
|
+
content: DYNAMAX_REMINDER.trim(),
|
|
119
|
+
display: false,
|
|
120
|
+
details: { dynamax: true, sticky: true },
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { WorkflowApi, WorkflowMeta, WorkflowModule } from "./types.ts";
|
|
3
|
+
import { parseWorkflowMeta } from "./workflow-module.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* v1 inline workflow source contract:
|
|
7
|
+
* - The script starts with `export const meta = { ... };` where the object is a pure literal.
|
|
8
|
+
* - The script contains exactly one `export default async ...` workflow function.
|
|
9
|
+
* - Inline scripts must not use `import` or any exports other than `meta` and the default function.
|
|
10
|
+
* - `Type` is injected lexically by the host; scripts must use that `Type`, not import typebox.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class InlineWorkflowCompileError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "InlineWorkflowCompileError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface InlineMetaLiteral {
|
|
21
|
+
readonly literal: string;
|
|
22
|
+
readonly value: unknown;
|
|
23
|
+
readonly endOffset: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type InlineWorkflowExecutor = (api: WorkflowApi, typebox: typeof Type) => Promise<unknown>;
|
|
27
|
+
type AsyncFunctionConstructor = new (...args: string[]) => InlineWorkflowExecutor;
|
|
28
|
+
|
|
29
|
+
const AsyncFunction = Object.getPrototypeOf(async function inlineWorkflowCompilerSentinel() {}).constructor as AsyncFunctionConstructor;
|
|
30
|
+
|
|
31
|
+
export function compileInlineWorkflow(source: string): WorkflowModule {
|
|
32
|
+
rejectForbiddenModuleSyntax(source);
|
|
33
|
+
const metaLiteral = extractMetaLiteral(source);
|
|
34
|
+
const parsedMeta = parseWorkflowMeta(metaLiteral.value);
|
|
35
|
+
if ("reason" in parsedMeta) throw new InlineWorkflowCompileError(parsedMeta.reason);
|
|
36
|
+
|
|
37
|
+
const defaultExpression = extractDefaultWorkflowExpression(source, metaLiteral.endOffset);
|
|
38
|
+
const executor = compileExecutor(defaultExpression);
|
|
39
|
+
return { meta: parsedMeta.meta, default: (api) => executor(api, Type) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extractMetaLiteral(source: string): InlineMetaLiteral {
|
|
43
|
+
const start = skipWhitespace(source, 0);
|
|
44
|
+
const prefix = /export\s+const\s+meta\s*=/y;
|
|
45
|
+
prefix.lastIndex = start;
|
|
46
|
+
const match = prefix.exec(source);
|
|
47
|
+
if (!match || match.index !== start) {
|
|
48
|
+
throw new InlineWorkflowCompileError("inline workflow must start with `export const meta = { ... }`;");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const objectStart = skipWhitespace(source, prefix.lastIndex);
|
|
52
|
+
if (source[objectStart] !== "{") {
|
|
53
|
+
throw new InlineWorkflowCompileError("inline workflow meta must be an object literal");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const parser = new LiteralParser(source, objectStart);
|
|
57
|
+
const value = parser.parseValue();
|
|
58
|
+
const literalEnd = parser.offset;
|
|
59
|
+
let endOffset = skipWhitespace(source, literalEnd);
|
|
60
|
+
if (source[endOffset] === ";") endOffset++;
|
|
61
|
+
|
|
62
|
+
return { literal: source.slice(objectStart, literalEnd), value, endOffset };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rejectForbiddenModuleSyntax(source: string): void {
|
|
66
|
+
if (/^\s*import\s/m.test(source)) {
|
|
67
|
+
throw new InlineWorkflowCompileError("inline workflows must not contain import statements; use injected Type instead");
|
|
68
|
+
}
|
|
69
|
+
if (/\bimport\s*\(/.test(source)) {
|
|
70
|
+
throw new InlineWorkflowCompileError("inline workflows must not use dynamic import()");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractDefaultWorkflowExpression(source: string, startOffset: number): string {
|
|
75
|
+
const moduleSource = source.slice(startOffset).trim();
|
|
76
|
+
if (!moduleSource) throw new InlineWorkflowCompileError("inline workflow must export a default async function");
|
|
77
|
+
if (!moduleSource.startsWith("export default")) {
|
|
78
|
+
throw new InlineWorkflowCompileError("inline workflow default export must directly follow the meta declaration");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const defaultPrefix = /^export\s+default\s+/;
|
|
82
|
+
const match = defaultPrefix.exec(moduleSource);
|
|
83
|
+
if (!match) throw new InlineWorkflowCompileError("inline workflow must export a default async function");
|
|
84
|
+
|
|
85
|
+
const expressionSource = moduleSource.slice(match[0].length).trimStart();
|
|
86
|
+
const bodyOpen = findDefaultBodyOpen(expressionSource);
|
|
87
|
+
const bodyClose = findMatchingDelimiter(expressionSource, bodyOpen, "{", "}");
|
|
88
|
+
const expression = expressionSource.slice(0, bodyClose + 1);
|
|
89
|
+
const remainder = expressionSource.slice(bodyClose + 1).trim();
|
|
90
|
+
if (remainder !== "" && remainder !== ";") {
|
|
91
|
+
throw new InlineWorkflowCompileError("inline workflow must not contain code after the default export");
|
|
92
|
+
}
|
|
93
|
+
return expression;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findDefaultBodyOpen(expressionSource: string): number {
|
|
97
|
+
const functionMatch = /^async\s+function(?:\s+[A-Za-z_$][\w$]*)?\s*/.exec(expressionSource);
|
|
98
|
+
if (functionMatch) {
|
|
99
|
+
const paramsOpen = skipWhitespace(expressionSource, functionMatch[0].length);
|
|
100
|
+
if (expressionSource[paramsOpen] !== "(") {
|
|
101
|
+
throw new InlineWorkflowCompileError("default async function must declare parameters with parentheses");
|
|
102
|
+
}
|
|
103
|
+
const paramsClose = findMatchingDelimiter(expressionSource, paramsOpen, "(", ")");
|
|
104
|
+
const bodyOpen = skipWhitespace(expressionSource, paramsClose + 1);
|
|
105
|
+
if (expressionSource[bodyOpen] !== "{") {
|
|
106
|
+
throw new InlineWorkflowCompileError("default async function must use a block body");
|
|
107
|
+
}
|
|
108
|
+
return bodyOpen;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const arrowMatch = /^async\s*/.exec(expressionSource);
|
|
112
|
+
if (arrowMatch) {
|
|
113
|
+
const paramsOpen = skipWhitespace(expressionSource, arrowMatch[0].length);
|
|
114
|
+
if (expressionSource[paramsOpen] !== "(") {
|
|
115
|
+
throw new InlineWorkflowCompileError("default async arrow workflow must declare parameters with parentheses");
|
|
116
|
+
}
|
|
117
|
+
const paramsClose = findMatchingDelimiter(expressionSource, paramsOpen, "(", ")");
|
|
118
|
+
let cursor = skipWhitespace(expressionSource, paramsClose + 1);
|
|
119
|
+
if (!expressionSource.startsWith("=>", cursor)) {
|
|
120
|
+
throw new InlineWorkflowCompileError("default async arrow workflow must use =>");
|
|
121
|
+
}
|
|
122
|
+
cursor = skipWhitespace(expressionSource, cursor + 2);
|
|
123
|
+
if (expressionSource[cursor] !== "{") {
|
|
124
|
+
throw new InlineWorkflowCompileError("default async arrow workflow must use a block body");
|
|
125
|
+
}
|
|
126
|
+
return cursor;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new InlineWorkflowCompileError("inline workflow default export must be an async function or async arrow function");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function compileExecutor(defaultExpression: string): InlineWorkflowExecutor {
|
|
133
|
+
try {
|
|
134
|
+
return new AsyncFunction("api", "Type", `"use strict";\nconst workflow = ${defaultExpression};\nreturn await workflow(api);`);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
throw new InlineWorkflowCompileError(`inline workflow default export did not compile: ${message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class LiteralParser {
|
|
142
|
+
private index: number;
|
|
143
|
+
|
|
144
|
+
constructor(private readonly source: string, offset: number) {
|
|
145
|
+
this.index = offset;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get offset(): number {
|
|
149
|
+
return this.index;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
parseValue(): unknown {
|
|
153
|
+
this.skipWhitespace();
|
|
154
|
+
const char = this.source[this.index];
|
|
155
|
+
if (char === "{") return this.parseObject();
|
|
156
|
+
if (char === "[") return this.parseArray();
|
|
157
|
+
if (char === '"' || char === "'") return this.parseString(char);
|
|
158
|
+
if (char === "-" || isDigit(char)) return this.parseNumber();
|
|
159
|
+
if (this.source.startsWith("true", this.index)) return this.consumeKeyword("true", true);
|
|
160
|
+
if (this.source.startsWith("false", this.index)) return this.consumeKeyword("false", false);
|
|
161
|
+
if (this.source.startsWith("null", this.index)) return this.consumeKeyword("null", null);
|
|
162
|
+
if (char === "`") this.fail("template literals are not allowed in inline workflow meta");
|
|
163
|
+
this.fail(`unexpected token in inline workflow meta: ${char ?? "end of input"}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private parseObject(): Record<string, unknown> {
|
|
167
|
+
const object: Record<string, unknown> = {};
|
|
168
|
+
this.expect("{");
|
|
169
|
+
this.skipWhitespace();
|
|
170
|
+
if (this.peek() === "}") {
|
|
171
|
+
this.index++;
|
|
172
|
+
return object;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
while (true) {
|
|
176
|
+
this.skipWhitespace();
|
|
177
|
+
if (this.source.startsWith("...", this.index)) this.fail("spread properties are not allowed in inline workflow meta");
|
|
178
|
+
if (this.peek() === "[") this.fail("computed properties are not allowed in inline workflow meta");
|
|
179
|
+
const key = this.parsePropertyKey();
|
|
180
|
+
this.skipWhitespace();
|
|
181
|
+
if (this.peek() !== ":") this.fail("inline workflow meta properties must use explicit key: value syntax");
|
|
182
|
+
this.index++;
|
|
183
|
+
object[key] = this.parseValue();
|
|
184
|
+
this.skipWhitespace();
|
|
185
|
+
const next = this.peek();
|
|
186
|
+
if (next === ",") {
|
|
187
|
+
this.index++;
|
|
188
|
+
this.skipWhitespace();
|
|
189
|
+
if (this.peek() === "}") {
|
|
190
|
+
this.index++;
|
|
191
|
+
return object;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (next === "}") {
|
|
196
|
+
this.index++;
|
|
197
|
+
return object;
|
|
198
|
+
}
|
|
199
|
+
this.fail("expected `,` or `}` in inline workflow meta object");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private parseArray(): unknown[] {
|
|
204
|
+
const array: unknown[] = [];
|
|
205
|
+
this.expect("[");
|
|
206
|
+
this.skipWhitespace();
|
|
207
|
+
if (this.peek() === "]") {
|
|
208
|
+
this.index++;
|
|
209
|
+
return array;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
while (true) {
|
|
213
|
+
array.push(this.parseValue());
|
|
214
|
+
this.skipWhitespace();
|
|
215
|
+
const next = this.peek();
|
|
216
|
+
if (next === ",") {
|
|
217
|
+
this.index++;
|
|
218
|
+
this.skipWhitespace();
|
|
219
|
+
if (this.peek() === "]") {
|
|
220
|
+
this.index++;
|
|
221
|
+
return array;
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (next === "]") {
|
|
226
|
+
this.index++;
|
|
227
|
+
return array;
|
|
228
|
+
}
|
|
229
|
+
this.fail("expected `,` or `]` in inline workflow meta array");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private parsePropertyKey(): string {
|
|
234
|
+
const char = this.peek();
|
|
235
|
+
if (char === '"' || char === "'") return this.parseString(char);
|
|
236
|
+
if (!isIdentifierStart(char)) this.fail("inline workflow meta property keys must be strings or identifiers");
|
|
237
|
+
const start = this.index;
|
|
238
|
+
this.index++;
|
|
239
|
+
while (isIdentifierPart(this.peek())) this.index++;
|
|
240
|
+
return this.source.slice(start, this.index);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private parseString(quote: string): string {
|
|
244
|
+
this.expect(quote);
|
|
245
|
+
let value = "";
|
|
246
|
+
while (this.index < this.source.length) {
|
|
247
|
+
const char = this.source[this.index];
|
|
248
|
+
if (char === quote) {
|
|
249
|
+
this.index++;
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
if (char === "\\") {
|
|
253
|
+
value += this.parseEscapeSequence();
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (char === "\n" || char === "\r") this.fail("inline workflow meta strings must not contain raw newlines");
|
|
257
|
+
value += char;
|
|
258
|
+
this.index++;
|
|
259
|
+
}
|
|
260
|
+
this.fail("unterminated string in inline workflow meta");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private parseEscapeSequence(): string {
|
|
264
|
+
this.expect("\\");
|
|
265
|
+
const escaped = this.source[this.index];
|
|
266
|
+
if (escaped === undefined) this.fail("unterminated escape sequence in inline workflow meta");
|
|
267
|
+
this.index++;
|
|
268
|
+
switch (escaped) {
|
|
269
|
+
case '"':
|
|
270
|
+
case "'":
|
|
271
|
+
case "\\":
|
|
272
|
+
case "/":
|
|
273
|
+
return escaped;
|
|
274
|
+
case "b":
|
|
275
|
+
return "\b";
|
|
276
|
+
case "f":
|
|
277
|
+
return "\f";
|
|
278
|
+
case "n":
|
|
279
|
+
return "\n";
|
|
280
|
+
case "r":
|
|
281
|
+
return "\r";
|
|
282
|
+
case "t":
|
|
283
|
+
return "\t";
|
|
284
|
+
case "u": {
|
|
285
|
+
const hex = this.source.slice(this.index, this.index + 4);
|
|
286
|
+
if (!/^[0-9A-Fa-f]{4}$/.test(hex)) this.fail("invalid unicode escape in inline workflow meta");
|
|
287
|
+
this.index += 4;
|
|
288
|
+
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
289
|
+
}
|
|
290
|
+
default:
|
|
291
|
+
this.fail(`unsupported escape sequence \\${escaped} in inline workflow meta`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private parseNumber(): number {
|
|
296
|
+
const match = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
297
|
+
match.lastIndex = this.index;
|
|
298
|
+
const parsed = match.exec(this.source);
|
|
299
|
+
if (!parsed) this.fail("invalid number in inline workflow meta");
|
|
300
|
+
this.index = match.lastIndex;
|
|
301
|
+
return Number(parsed[0]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private consumeKeyword<T>(keyword: string, value: T): T {
|
|
305
|
+
this.index += keyword.length;
|
|
306
|
+
if (isIdentifierPart(this.peek())) this.fail(`unexpected identifier after ${keyword} in inline workflow meta`);
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private skipWhitespace(): void {
|
|
311
|
+
this.index = skipWhitespace(this.source, this.index);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private peek(): string | undefined {
|
|
315
|
+
return this.source[this.index];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private expect(char: string): void {
|
|
319
|
+
if (this.source[this.index] !== char) this.fail(`expected ${char} in inline workflow meta`);
|
|
320
|
+
this.index++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private fail(message: string): never {
|
|
324
|
+
throw new InlineWorkflowCompileError(`${message} at offset ${this.index}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function findMatchingDelimiter(source: string, openIndex: number, open: string, close: string): number {
|
|
329
|
+
let depth = 1;
|
|
330
|
+
let index = openIndex + 1;
|
|
331
|
+
while (index < source.length) {
|
|
332
|
+
const char = source[index];
|
|
333
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
334
|
+
index = skipQuotedSource(source, index, char);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
338
|
+
index = skipLineComment(source, index);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
342
|
+
index = skipBlockComment(source, index);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (char === open) depth++;
|
|
346
|
+
if (char === close) {
|
|
347
|
+
depth--;
|
|
348
|
+
if (depth === 0) return index;
|
|
349
|
+
}
|
|
350
|
+
index++;
|
|
351
|
+
}
|
|
352
|
+
throw new InlineWorkflowCompileError(`unterminated ${open}${close} block in inline workflow default export`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function skipQuotedSource(source: string, start: number, quote: string): number {
|
|
356
|
+
let index = start + 1;
|
|
357
|
+
while (index < source.length) {
|
|
358
|
+
const char = source[index];
|
|
359
|
+
if (char === "\\") {
|
|
360
|
+
index += 2;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (char === quote) return index + 1;
|
|
364
|
+
index++;
|
|
365
|
+
}
|
|
366
|
+
throw new InlineWorkflowCompileError("unterminated string in inline workflow default export");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function skipLineComment(source: string, start: number): number {
|
|
370
|
+
const newline = source.indexOf("\n", start + 2);
|
|
371
|
+
return newline === -1 ? source.length : newline + 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function skipBlockComment(source: string, start: number): number {
|
|
375
|
+
const end = source.indexOf("*/", start + 2);
|
|
376
|
+
if (end === -1) throw new InlineWorkflowCompileError("unterminated block comment in inline workflow default export");
|
|
377
|
+
return end + 2;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function skipWhitespace(source: string, start: number): number {
|
|
381
|
+
let index = start;
|
|
382
|
+
while (index < source.length && /\s/.test(source[index] ?? "")) index++;
|
|
383
|
+
return index;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isDigit(value: string | undefined): boolean {
|
|
387
|
+
return value !== undefined && value >= "0" && value <= "9";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isIdentifierStart(value: string | undefined): boolean {
|
|
391
|
+
return value !== undefined && /[A-Za-z_$]/.test(value);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isIdentifierPart(value: string | undefined): boolean {
|
|
395
|
+
return value !== undefined && /[A-Za-z0-9_$]/.test(value);
|
|
396
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { WorkflowMeta, WorkflowModule } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type WorkflowMetaCandidate = {
|
|
4
|
+
readonly name?: unknown;
|
|
5
|
+
readonly description?: unknown;
|
|
6
|
+
readonly phases?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type WorkflowModuleCandidate = {
|
|
10
|
+
readonly meta?: unknown;
|
|
11
|
+
readonly default?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Validate and normalize workflow metadata without executing workflow code. */
|
|
15
|
+
export function parseWorkflowMeta(value: unknown): { meta: WorkflowMeta } | { reason: string } {
|
|
16
|
+
if (!isRecord(value)) return { reason: "meta export must be an object" };
|
|
17
|
+
return parseWorkflowMetaObject(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseWorkflowModule(value: unknown): { module: WorkflowModule } | { reason: string } {
|
|
21
|
+
if (!isRecord(value)) return { reason: "module export is not an object" };
|
|
22
|
+
const candidate = value as WorkflowModuleCandidate;
|
|
23
|
+
const meta = candidate.meta;
|
|
24
|
+
if (!isRecord(meta)) return { reason: "missing meta export" };
|
|
25
|
+
|
|
26
|
+
const parsedMeta = parseWorkflowMetaObject(meta);
|
|
27
|
+
if ("reason" in parsedMeta) return parsedMeta;
|
|
28
|
+
if (!isWorkflowRun(candidate.default)) return { reason: "default export must be a function" };
|
|
29
|
+
|
|
30
|
+
return { module: { meta: parsedMeta.meta, default: candidate.default } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseWorkflowMetaObject(meta: WorkflowMetaCandidate): { meta: WorkflowMeta } | { reason: string } {
|
|
34
|
+
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
35
|
+
|
|
36
|
+
const description = meta.description;
|
|
37
|
+
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
38
|
+
|
|
39
|
+
const phases = meta.phases;
|
|
40
|
+
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
41
|
+
|
|
42
|
+
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
43
|
+
if (phases !== undefined) workflowMeta.phases = phases;
|
|
44
|
+
return { meta: workflowMeta };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
48
|
+
return typeof value === "function";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isWorkflowPhases(value: unknown): value is Array<{ title: string }> {
|
|
52
|
+
return Array.isArray(value) && value.every((phase) => isRecord(phase) && typeof phase.title === "string");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null;
|
|
57
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { WorkflowModule } from "./types.ts";
|
|
2
2
|
import * as codeReview from "../workflows/code-review.ts";
|
|
3
|
-
import * as ping from "../workflows/ping.ts";
|
|
4
3
|
import * as refactorScout from "../workflows/refactor-scout.ts";
|
|
5
4
|
import * as diagnose from "../workflows/diagnose.ts";
|
|
6
5
|
import * as perfReview from "../workflows/perf-review.ts";
|
|
@@ -19,6 +18,6 @@ import * as perfReview from "../workflows/perf-review.ts";
|
|
|
19
18
|
* dynamically imported built-ins. Startup optimizations should happen at the extension
|
|
20
19
|
* entrypoint/discovery boundary first.
|
|
21
20
|
*/
|
|
22
|
-
export const BUILTIN_WORKFLOWS: WorkflowModule[] = [codeReview,
|
|
23
|
-
export const BUILTIN_WORKFLOW_FILES = new Set(["code-review.ts", "
|
|
21
|
+
export const BUILTIN_WORKFLOWS: WorkflowModule[] = [codeReview, refactorScout, diagnose, perfReview];
|
|
22
|
+
export const BUILTIN_WORKFLOW_FILES = new Set(["code-review.ts", "refactor-scout.ts", "diagnose.ts", "perf-review.ts"]);
|
|
24
23
|
export const BUILTIN_WORKFLOW_NAMES = BUILTIN_WORKFLOWS.map((mod) => mod.meta.name);
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ pi remove git:github.com/timbrinded/pi-workflow-engine
|
|
|
55
55
|
|
|
56
56
|
## Usage
|
|
57
57
|
|
|
58
|
+
For the complete installation, invocation, authoring, tuning, and troubleshooting guide, see [USAGE.md](USAGE.md).
|
|
59
|
+
|
|
58
60
|
In a pi session, from inside a git repo with changes:
|
|
59
61
|
|
|
60
62
|
```text
|
|
@@ -64,15 +66,25 @@ In a pi session, from inside a git repo with changes:
|
|
|
64
66
|
/workflow diagnose "typecheck fails after the schema change"
|
|
65
67
|
/workflow perf-review "workflow startup latency"
|
|
66
68
|
/workflow code-review --inspect # open the live workflow inspector
|
|
67
|
-
/workflow ping # quick engine smoke test
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
The host agent can also invoke the `workflow` tool mid-conversation
|
|
71
|
+
The host agent can also invoke the `workflow` tool mid-conversation. It accepts either a registered workflow `name` or a one-off inline workflow `script`:
|
|
71
72
|
|
|
72
73
|
```text
|
|
73
74
|
Run the code-review workflow on this PR and use the result before deciding what to fix.
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
Opt into dynamic multi-agent orchestration with the literal `dynamax` token, or keep it sticky for the session:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
dynamax inspect this bug with multiple focused agents
|
|
81
|
+
/dynamax on
|
|
82
|
+
/dynamax status
|
|
83
|
+
/dynamax off
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`dynamax` is a permission signal for the host agent: once opted in, it may run an existing named workflow or author an inline workflow script through the `workflow` tool. In interactive mode, `/workflow` with no arguments also offers `✍ Author temporary one-shot workflow…`; choose it, type a brief, and pi will ask the host agent to author/run a temporary inline workflow.
|
|
87
|
+
|
|
76
88
|
The advisory workflows inspect and report only; they do not edit files. They return the same shape: summary, ranked findings, evidence, impact, recommendations, and next steps.
|
|
77
89
|
|
|
78
90
|
## Built-in workflows
|
|
@@ -81,7 +93,6 @@ The advisory workflows inspect and report only; they do not edit files. They ret
|
|
|
81
93
|
- `refactor-scout`: Looks for small, safe refactor opportunities: duplication, complexity, weak types, boundary leaks, dead code, and convention drift.
|
|
82
94
|
- `diagnose`: Investigates a symptom, failing command, or regression. It generates competing root-cause hypotheses, verifies them, and returns the most likely causes with next validation steps.
|
|
83
95
|
- `perf-review`: Reviews a slow path or workload for bottleneck hypotheses, measurement gaps, and safe optimization directions. It avoids claiming certainty when measurement evidence is missing.
|
|
84
|
-
- `ping`: Minimal smoke workflow for checking that the engine can run one structured agent call.
|
|
85
96
|
|
|
86
97
|
## The code-review workflow
|
|
87
98
|
|
|
@@ -135,12 +146,33 @@ export default async function run({ agent, parallel, phase }: WorkflowApi) {
|
|
|
135
146
|
}
|
|
136
147
|
```
|
|
137
148
|
|
|
149
|
+
Inline workflow scripts use the same primitives, but are passed as a string to the `workflow` tool instead of being saved as files. A minimal inline workflow starts with `export const meta` and uses the injected Type object for schemas:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
export const meta = {
|
|
153
|
+
name: "inline-review",
|
|
154
|
+
description: "One-off focused review workflow.",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default async function run({ agent, parallel, phase, args }) {
|
|
158
|
+
phase("Find");
|
|
159
|
+
const Finding = Type.Object({ summary: Type.String() });
|
|
160
|
+
const findings = await parallel([
|
|
161
|
+
() => agent(`Find correctness issues: ${args}`, { schema: Finding, thinkingLevel: "low" }),
|
|
162
|
+
() => agent(`Find edge cases: ${args}`, { schema: Finding, thinkingLevel: "low" }),
|
|
163
|
+
]);
|
|
164
|
+
return { summary: JSON.stringify(findings) };
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Inline v1 rules: no imports, no dynamic `import()`, pure-literal `meta`, schemas must use the injected Type value, and scripts run in-process with the extension's permissions. This v1 path is permissive rather than sandboxed.
|
|
169
|
+
|
|
138
170
|
Under the hood:
|
|
139
171
|
|
|
140
172
|
- **Each `agent()` is an in-process pi `AgentSession`** using `createAgentSession` and `SessionManager.inMemory()`.
|
|
141
173
|
- **Structured output is a terminating tool**. The engine registers one tool whose `parameters` is your schema; pi validates the call and the engine captures the args. There is no JSON scraping.
|
|
142
174
|
- **A single global semaphore caps concurrent agents**, so `parallel` and `pipeline` can nest freely while every `agent()` call still respects the run cap.
|
|
143
|
-
- **
|
|
175
|
+
- **Three surfaces are registered**: `/workflow <name> [args]` for direct use, `/dynamax on|off|status` for sticky orchestration opt-in, and a `workflow` tool for host-agent delegation by `name` or inline `script`.
|
|
144
176
|
|
|
145
177
|
## Local development
|
|
146
178
|
|
|
@@ -177,11 +209,7 @@ bun run bench:startup -- --json
|
|
|
177
209
|
bun run bench:ui -- --agents 1000 --lane-items 1000 --json
|
|
178
210
|
```
|
|
179
211
|
|
|
180
|
-
Add `--out` to write machine-local JSON under `.artifacts/benchmarks/`. These timings are advisory, not portable thresholds.
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
pi -e . -p "/workflow ping"
|
|
184
|
-
```
|
|
212
|
+
Add `--out` to write machine-local JSON under `.artifacts/benchmarks/`. These timings are advisory, not portable thresholds.
|
|
185
213
|
|
|
186
214
|
Guardrails:
|
|
187
215
|
|
|
@@ -192,7 +220,7 @@ Guardrails:
|
|
|
192
220
|
Load your working copy through the package manifest without installing it. This is ephemeral and exercises the same `.pi/extensions/pi-workflow-engine/index.ts` entrypoint that installed packages use:
|
|
193
221
|
|
|
194
222
|
```bash
|
|
195
|
-
pi -e .
|
|
223
|
+
pi -e .
|
|
196
224
|
```
|
|
197
225
|
|
|
198
226
|
This repo also includes `.pi/settings.json` for project-local auto-discovery. If you also have the global package installed, pi may report duplicate `/workflow` or `workflow` tool diagnostics; remove one source or force pi to ignore discovered extensions and load this working copy instead:
|
|
@@ -227,7 +255,6 @@ Tune the built-in review workflow by editing the `ANGLES` array in `.pi/extensio
|
|
|
227
255
|
refactor-scout.ts advisory refactor opportunities
|
|
228
256
|
diagnose.ts advisory bug diagnosis
|
|
229
257
|
perf-review.ts advisory performance investigation
|
|
230
|
-
ping.ts minimal smoke workflow
|
|
231
258
|
```
|
|
232
259
|
|
|
233
260
|
## License
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
`pi-workflow-engine` adds multi-agent workflows to pi. Most users only need two surfaces:
|
|
4
|
+
|
|
5
|
+
- `/workflow <name> [args]` to run a saved workflow directly.
|
|
6
|
+
- `dynamax` when you want the host agent to author a one-off inline workflow for investigation/review.
|
|
7
|
+
|
|
8
|
+
## Run a workflow
|
|
9
|
+
|
|
10
|
+
From inside a repo:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
/workflow code-review
|
|
14
|
+
/workflow code-review HEAD~3
|
|
15
|
+
/workflow refactor-scout src/
|
|
16
|
+
/workflow diagnose "typecheck fails after the schema change"
|
|
17
|
+
/workflow perf-review "workflow startup latency"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The built-in workflows are advisory. They inspect, verify, and report; they do **not** edit files.
|
|
21
|
+
|
|
22
|
+
Useful flags:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
/workflow code-review --inspect # show live inspector
|
|
26
|
+
/workflow code-review --concurrency=4 # cap concurrent subagents
|
|
27
|
+
/workflow code-review --refresh # rediscover newly added workflow files
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Built-in workflows
|
|
31
|
+
|
|
32
|
+
| Name | Purpose |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `code-review` | Reviews a PR, branch diff, ref range, or focus area. |
|
|
35
|
+
| `refactor-scout` | Finds small, safe refactor opportunities. |
|
|
36
|
+
| `diagnose` | Investigates a bug, failing command, or regression. |
|
|
37
|
+
| `perf-review` | Reviews a slow path or performance concern. |
|
|
38
|
+
|
|
39
|
+
`code-review` auto-detects a diff when you do not pass args. If there is an open GitHub PR for the current branch, it uses that PR diff; otherwise it falls back to branch-vs-main/master or `HEAD~1`.
|
|
40
|
+
|
|
41
|
+
## Use `dynamax` for inline workflows
|
|
42
|
+
|
|
43
|
+
Use `dynamax` when the built-in workflows are not specific enough and you want the main pi agent to author a temporary workflow for the current question.
|
|
44
|
+
|
|
45
|
+
Interactive shortcut: run `/workflow` with no arguments, choose `✍ Author temporary one-shot workflow…`, and type a brief. pi will send that brief back to the host agent as a `dynamax` request so it can author and run an inline workflow with the `workflow` tool's `script` argument.
|
|
46
|
+
|
|
47
|
+
That inline workflow should investigate, fan out, verify, and summarize. It is **not** where code edits happen. If you want changes made, use the workflow result as evidence and then ask pi to edit separately.
|
|
48
|
+
|
|
49
|
+
One-off examples:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
dynamax author an inline workflow to investigate why typecheck is failing
|
|
53
|
+
dynamax create a one-off workflow to compare the parser and tests before we change code
|
|
54
|
+
dynamax use parallel agents to review this design and report the risks
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Sticky for the session:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
/dynamax on
|
|
61
|
+
/dynamax off
|
|
62
|
+
/dynamax status
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With dynamax enabled, the host agent usually calls the `workflow` tool with `script`: a one-off inline workflow. It can still call a saved workflow by `name` when one already fits.
|
|
66
|
+
|
|
67
|
+
## Workflow results
|
|
68
|
+
|
|
69
|
+
Workflow results are rendered in pi with:
|
|
70
|
+
|
|
71
|
+
- summary;
|
|
72
|
+
- ranked findings;
|
|
73
|
+
- locations and evidence;
|
|
74
|
+
- recommendations;
|
|
75
|
+
- next steps;
|
|
76
|
+
- run stats when available.
|
|
77
|
+
|
|
78
|
+
During a run, pi shows live phases and subagent status. Use `--inspect` if you want a larger live view.
|
|
79
|
+
|
|
80
|
+
## Author a saved workflow
|
|
81
|
+
|
|
82
|
+
Saved workflows are TypeScript modules with `meta` plus a default async function:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Type } from "typebox";
|
|
86
|
+
import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
87
|
+
|
|
88
|
+
export const meta: WorkflowMeta = {
|
|
89
|
+
name: "my-workflow",
|
|
90
|
+
description: "Find and summarize something important.",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const Finding = Type.Object({
|
|
94
|
+
summary: Type.String(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default async function run({ agent, parallel, phase, args }: WorkflowApi) {
|
|
98
|
+
phase("Find");
|
|
99
|
+
const findings = await parallel([
|
|
100
|
+
() => agent(`Find correctness issues: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
|
|
101
|
+
() => agent(`Find edge cases: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
phase("Synthesize");
|
|
105
|
+
return agent(`Summarize: ${JSON.stringify(findings)}`, { thinkingLevel: "medium" });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Core primitives:
|
|
110
|
+
|
|
111
|
+
| Primitive | What it does |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `agent(prompt, opts)` | Runs one isolated subagent. With `schema`, returns validated structured data. |
|
|
114
|
+
| `parallel(thunks)` | Runs thunks concurrently and waits for all. |
|
|
115
|
+
| `pipeline(items, ...stages)` | Runs each item through stages independently. |
|
|
116
|
+
| `phase(title)` / `log(message)` | Updates workflow progress UI. |
|
|
117
|
+
| `progress(event)` | Emits counters, summaries, and lane items. |
|
|
118
|
+
|
|
119
|
+
Set `thinkingLevel` on fan-out agents. Otherwise many subagents can inherit an expensive global reasoning level.
|
|
120
|
+
|
|
121
|
+
### Where workflows live
|
|
122
|
+
|
|
123
|
+
Guaranteed built-ins are statically registered:
|
|
124
|
+
|
|
125
|
+
1. Add `.pi/extensions/pi-workflow-engine/workflows/<name>.ts`.
|
|
126
|
+
2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
|
|
127
|
+
3. Add it to `BUILTIN_WORKFLOWS`.
|
|
128
|
+
|
|
129
|
+
Drop-in workflows are also discovered best-effort from:
|
|
130
|
+
|
|
131
|
+
- `.pi/extensions/pi-workflow-engine/workflows/*.ts`
|
|
132
|
+
- `~/.pi/agent/workflows/*.ts`
|
|
133
|
+
|
|
134
|
+
Use `/workflow <name> --refresh` after adding a drop-in file.
|
|
135
|
+
|
|
136
|
+
## Author an inline workflow
|
|
137
|
+
|
|
138
|
+
Inline workflows are passed to the `workflow` tool as a script string. They are useful for one-off dynamax orchestration.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
export const meta = {
|
|
142
|
+
name: "inline-review",
|
|
143
|
+
description: "One-off focused review.",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default async function run({ agent, parallel, phase, args }) {
|
|
147
|
+
phase("Find");
|
|
148
|
+
const Finding = Type.Object({ summary: Type.String() });
|
|
149
|
+
const findings = await parallel([
|
|
150
|
+
() => agent(`Find correctness issues: ${args}`, { schema: Finding, thinkingLevel: "low" }),
|
|
151
|
+
() => agent(`Find edge cases: ${args}`, { schema: Finding, thinkingLevel: "low" }),
|
|
152
|
+
]);
|
|
153
|
+
return { summary: JSON.stringify(findings) };
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inline rules:
|
|
158
|
+
|
|
159
|
+
- start with `export const meta = { ... }`;
|
|
160
|
+
- default-export an async workflow function;
|
|
161
|
+
- no imports or dynamic `import()`;
|
|
162
|
+
- use the injected `Type` object for schemas;
|
|
163
|
+
- no extra exports or code after the default export.
|
|
164
|
+
|
|
165
|
+
Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
|
|
166
|
+
|
|
167
|
+
## Runtime knobs
|
|
168
|
+
|
|
169
|
+
Only tune these when a workflow is too slow, too expensive, or too noisy:
|
|
170
|
+
|
|
171
|
+
| Knob | Meaning |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
|
|
174
|
+
| `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
|
|
175
|
+
| `--perf` / `PI_WORKFLOW_PERF=1` | Include internal timing aggregates. |
|
|
176
|
+
| `PI_WORKFLOW_LANE_ITEM_LIMIT=N` | Cap retained progress lane items. |
|
|
177
|
+
|
|
178
|
+
## Common fixes
|
|
179
|
+
|
|
180
|
+
- **Unknown workflow**: confirm the extension is loaded with `pi list`; use `--refresh` for new drop-ins.
|
|
181
|
+
- **Inline compile error**: check the inline rules above.
|
|
182
|
+
- **Slow run**: lower fan-out, set `thinkingLevel`, or reduce `--concurrency`.
|
|
183
|
+
- **Duplicate command/tool warnings while developing**: avoid loading both the global package and the working copy.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-workflow-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A customisable multi-agent workflow-orchestration engine for the pi coding agent.",
|
|
6
6
|
"keywords": [
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
".pi/settings.json",
|
|
26
26
|
"assets",
|
|
27
27
|
"README.md",
|
|
28
|
+
"USAGE.md",
|
|
28
29
|
"LICENSE"
|
|
29
30
|
],
|
|
30
31
|
"pi": {
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
3
|
-
|
|
4
|
-
/** Minimal engine smoke test: a single structured-output agent call. Fast + cheap. */
|
|
5
|
-
export const meta: WorkflowMeta = {
|
|
6
|
-
name: "ping",
|
|
7
|
-
description: "Engine smoke test — one structured agent call, no tools.",
|
|
8
|
-
phases: [{ title: "Ping" }],
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
12
|
-
const { agent, phase, log } = api;
|
|
13
|
-
phase("Ping");
|
|
14
|
-
log("calling one structured agent");
|
|
15
|
-
const result = await agent(
|
|
16
|
-
"You are a code-review workflow engine. Return a one-word headline and a one-sentence summary of what you do.",
|
|
17
|
-
{
|
|
18
|
-
phase: "Ping",
|
|
19
|
-
label: "ping",
|
|
20
|
-
tools: [],
|
|
21
|
-
thinkingLevel: "low",
|
|
22
|
-
schema: Type.Object({
|
|
23
|
-
headline: Type.String({ description: "One word" }),
|
|
24
|
-
summary: Type.String({ description: "One sentence" }),
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
);
|
|
28
|
-
return result ?? { headline: "none", summary: "no structured answer returned" };
|
|
29
|
-
}
|