pi-extensible-workflows 3.1.0 → 3.2.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 +11 -18
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +44 -4
- package/dist/src/host.js +843 -395
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/validation.js +34 -0
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +21 -28
- package/src/execution.ts +13 -4
- package/src/host.ts +702 -362
- package/src/index.ts +1 -0
- package/src/types.ts +1 -1
- package/src/validation.ts +27 -0
- package/src/workflow-artifacts.ts +34 -0
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./validation.js";
|
|
|
5
5
|
export * from "./registry.js";
|
|
6
6
|
export * from "./execution.js";
|
|
7
7
|
export * from "./host.js";
|
|
8
|
+
export * from "./workflow-artifacts.js";
|
|
8
9
|
export { default } from "./host.js";
|
|
9
10
|
export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
10
11
|
export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
|
package/src/types.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelS
|
|
|
73
73
|
export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
|
|
74
74
|
export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
|
|
75
75
|
export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
|
|
76
|
-
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
|
|
76
|
+
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
|
|
77
77
|
export interface WorkflowRunEvent { type: string; message: string }
|
|
78
78
|
export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
|
|
79
79
|
export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
|
package/src/validation.ts
CHANGED
|
@@ -324,6 +324,32 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
|
|
|
324
324
|
visit(program, { execution: "sequential", structure: [] });
|
|
325
325
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
326
326
|
}
|
|
327
|
+
function memberCall(node: acorn.AnyNode | undefined, objectName: string, propertyName: string): boolean {
|
|
328
|
+
if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.object.name !== objectName || node.callee.property.type !== "Identifier") return false;
|
|
329
|
+
return node.callee.property.name === propertyName;
|
|
330
|
+
}
|
|
331
|
+
function mapCallback(node: acorn.AnyNode): acorn.AnyNode | undefined {
|
|
332
|
+
if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled")) return undefined;
|
|
333
|
+
if (node.type !== "CallExpression") return undefined;
|
|
334
|
+
const source = node.arguments[0];
|
|
335
|
+
if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name)) return undefined;
|
|
336
|
+
const callback = source.arguments[0];
|
|
337
|
+
return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
|
|
338
|
+
}
|
|
339
|
+
function hasUnscopedAgent(node: acorn.AnyNode, scoped = false): boolean {
|
|
340
|
+
const operation = workflowCallKind(node);
|
|
341
|
+
if (operation === "agent") return !scoped;
|
|
342
|
+
const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
|
|
343
|
+
return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
|
|
344
|
+
}
|
|
345
|
+
function validateObviousConcurrentAgentCalls(program: acorn.Program): void {
|
|
346
|
+
const visit = (node: acorn.AnyNode): void => {
|
|
347
|
+
const callback = mapCallback(node);
|
|
348
|
+
if (callback && hasUnscopedAgent(callback)) fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
|
|
349
|
+
for (const child of astChildren(node)) visit(child);
|
|
350
|
+
};
|
|
351
|
+
visit(program);
|
|
352
|
+
}
|
|
327
353
|
function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
|
|
328
354
|
const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
|
|
329
355
|
if (node.type === "Identifier" && node.name === name) {
|
|
@@ -606,6 +632,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
606
632
|
validateDirectPrimitiveReferences(program, "shell");
|
|
607
633
|
for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
|
|
608
634
|
const calls = workflowCalls(program);
|
|
635
|
+
validateObviousConcurrentAgentCalls(program);
|
|
609
636
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
|
|
610
637
|
for (const call of calls) {
|
|
611
638
|
const operation = call.callee.name;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { JsonValue } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowArtifact { extension: ".js" | ".json" | ".md"; content: string }
|
|
8
|
+
export type WorkflowTui = { stop(): void; start(): void; requestRender(force?: boolean): void };
|
|
9
|
+
|
|
10
|
+
export function workflowScriptArtifact(script: string): WorkflowArtifact { return { extension: ".js", content: script }; }
|
|
11
|
+
export function workflowResultArtifact(value: JsonValue): WorkflowArtifact { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
|
|
12
|
+
|
|
13
|
+
async function spawnWorkflowEditor(command: string, path: string): Promise<number | null> {
|
|
14
|
+
const [editor, ...editorArgs] = command.split(" ");
|
|
15
|
+
if (!editor) return null;
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
try {
|
|
18
|
+
const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
|
|
19
|
+
child.once("error", () => { resolve(null); });
|
|
20
|
+
child.once("close", (code) => { resolve(code); });
|
|
21
|
+
} catch { resolve(null); }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null> {
|
|
26
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
|
|
27
|
+
const path = join(directory, `artifact${artifact.extension}`);
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
|
|
30
|
+
tui.stop();
|
|
31
|
+
try { return await spawnWorkflowEditor(command, path); }
|
|
32
|
+
finally { tui.start(); tui.requestRender(true); }
|
|
33
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
34
|
+
}
|