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/dist/src/index.d.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/dist/src/index.js
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 { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
package/dist/src/types.d.ts
CHANGED
package/dist/src/validation.js
CHANGED
|
@@ -401,6 +401,39 @@ function workflowCallsWithStructure(program) {
|
|
|
401
401
|
visit(program, { execution: "sequential", structure: [] });
|
|
402
402
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
403
403
|
}
|
|
404
|
+
function memberCall(node, objectName, propertyName) {
|
|
405
|
+
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")
|
|
406
|
+
return false;
|
|
407
|
+
return node.callee.property.name === propertyName;
|
|
408
|
+
}
|
|
409
|
+
function mapCallback(node) {
|
|
410
|
+
if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled"))
|
|
411
|
+
return undefined;
|
|
412
|
+
if (node.type !== "CallExpression")
|
|
413
|
+
return undefined;
|
|
414
|
+
const source = node.arguments[0];
|
|
415
|
+
if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name))
|
|
416
|
+
return undefined;
|
|
417
|
+
const callback = source.arguments[0];
|
|
418
|
+
return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
|
|
419
|
+
}
|
|
420
|
+
function hasUnscopedAgent(node, scoped = false) {
|
|
421
|
+
const operation = workflowCallKind(node);
|
|
422
|
+
if (operation === "agent")
|
|
423
|
+
return !scoped;
|
|
424
|
+
const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
|
|
425
|
+
return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
|
|
426
|
+
}
|
|
427
|
+
function validateObviousConcurrentAgentCalls(program) {
|
|
428
|
+
const visit = (node) => {
|
|
429
|
+
const callback = mapCallback(node);
|
|
430
|
+
if (callback && hasUnscopedAgent(callback))
|
|
431
|
+
fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
|
|
432
|
+
for (const child of astChildren(node))
|
|
433
|
+
visit(child);
|
|
434
|
+
};
|
|
435
|
+
visit(program);
|
|
436
|
+
}
|
|
404
437
|
function validateDirectPrimitiveReferences(program, name) {
|
|
405
438
|
const visit = (node, parent) => {
|
|
406
439
|
if (node.type === "Identifier" && node.name === name) {
|
|
@@ -731,6 +764,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
731
764
|
for (const [index, schema] of schemas.entries())
|
|
732
765
|
validateSchema(schema, `schema[${String(index)}]`);
|
|
733
766
|
const calls = workflowCalls(program);
|
|
767
|
+
validateObviousConcurrentAgentCalls(program);
|
|
734
768
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase) => phase !== undefined);
|
|
735
769
|
for (const call of calls) {
|
|
736
770
|
const operation = call.callee.name;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JsonValue } from "./types.js";
|
|
2
|
+
export interface WorkflowArtifact {
|
|
3
|
+
extension: ".js" | ".json" | ".md";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export type WorkflowTui = {
|
|
7
|
+
stop(): void;
|
|
8
|
+
start(): void;
|
|
9
|
+
requestRender(force?: boolean): void;
|
|
10
|
+
};
|
|
11
|
+
export declare function workflowScriptArtifact(script: string): WorkflowArtifact;
|
|
12
|
+
export declare function workflowResultArtifact(value: JsonValue): WorkflowArtifact;
|
|
13
|
+
export declare function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null>;
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export function workflowScriptArtifact(script) { return { extension: ".js", content: script }; }
|
|
6
|
+
export function workflowResultArtifact(value) { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
|
|
7
|
+
async function spawnWorkflowEditor(command, path) {
|
|
8
|
+
const [editor, ...editorArgs] = command.split(" ");
|
|
9
|
+
if (!editor)
|
|
10
|
+
return null;
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
|
|
14
|
+
child.once("error", () => { resolve(null); });
|
|
15
|
+
child.once("close", (code) => { resolve(code); });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
resolve(null);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function openWorkflowArtifact(tui, command, artifact) {
|
|
23
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
|
|
24
|
+
const path = join(directory, `artifact${artifact.extension}`);
|
|
25
|
+
try {
|
|
26
|
+
await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
tui.stop();
|
|
28
|
+
try {
|
|
29
|
+
return await spawnWorkflowEditor(command, path);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
tui.start();
|
|
33
|
+
tui.requestRender(true);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await rm(directory, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Workflow extension template
|
|
2
|
+
|
|
3
|
+
This is a small, copyable extension rather than a generator. It shows the usual
|
|
4
|
+
registration shape with one function and one packaged role. Copy the directory
|
|
5
|
+
into a project, rename the metadata and function, then edit the role body.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
From the repository root after installing dependencies and building the package:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
node --test examples/workflow-extension-template/extension.test.mjs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For a published package, run the same test from this directory after installing
|
|
17
|
+
`pi-extensible-workflows` in the surrounding project. Copy the directory into a
|
|
18
|
+
trusted Pi extension location; Pi auto-discovers its `index.js` entry point.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `index.js` registers `greet` and resolves `roles/` from `import.meta.url`.
|
|
24
|
+
Relative role-directory strings are not accepted by the extension API.
|
|
25
|
+
- `roles/reviewer.md` is a portable packaged role with no provider or tool
|
|
26
|
+
assumptions.
|
|
27
|
+
- `extension.test.mjs` checks registration, function behavior, role packaging,
|
|
28
|
+
and the advanced examples.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Optional advanced pieces
|
|
32
|
+
|
|
33
|
+
The dynamic `template-model` alias and `templateAdvisor` setup hook are
|
|
34
|
+
optional examples. The hook only changes an agent after the call includes
|
|
35
|
+
`{ templateAdvisor: true }`; remove either section if the extension does not
|
|
36
|
+
need it. Both features are trusted host code and should be kept under explicit
|
|
37
|
+
project policy.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { beginWorkflowExtensionLoading, loadingRegistry, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, resetWorkflowRegistry, workflowCatalog } from "pi-extensible-workflows";
|
|
10
|
+
|
|
11
|
+
test("discovers the copied directory as a trusted Pi extension", async () => {
|
|
12
|
+
const root = await mkdtemp(join(tmpdir(), "workflow-extension-template-"));
|
|
13
|
+
try {
|
|
14
|
+
const destination = join(root, ".pi", "extensions", "workflow-extension-template");
|
|
15
|
+
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
16
|
+
const packageEntry = fileURLToPath(import.meta.resolve("pi-extensible-workflows"));
|
|
17
|
+
const packageRoot = join(dirname(packageEntry), "..", "..");
|
|
18
|
+
await symlink(packageRoot, join(root, "node_modules", "pi-extensible-workflows"), "dir");
|
|
19
|
+
await cp(dirname(fileURLToPath(import.meta.url)), destination, { recursive: true });
|
|
20
|
+
const result = await discoverAndLoadExtensions([], root, join(root, ".pi", "agent"));
|
|
21
|
+
assert.equal(result.errors.length, 0);
|
|
22
|
+
assert.equal(result.extensions.length, 1);
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resetWorkflowRegistry();
|
|
28
|
+
const { default: extension } = await import("./index.js");
|
|
29
|
+
beginWorkflowExtensionLoading();
|
|
30
|
+
extension();
|
|
31
|
+
|
|
32
|
+
const catalog = workflowCatalog();
|
|
33
|
+
assert.deepEqual(catalog.functions.map(({ name }) => name), ["greet"]);
|
|
34
|
+
assert.deepEqual(catalog.modelAliasEntries?.filter(({ name }) => name === "template-model").map(({ name, kind }) => ({ name, kind })), [{ name: "template-model", kind: "dynamic" }]);
|
|
35
|
+
assert.equal(await registeredWorkflowFunctions().greet.run({ name: "Ada" }, {}), "Hello, Ada!");
|
|
36
|
+
|
|
37
|
+
const registration = registeredWorkflowRoleDirectoryRegistrations()[0];
|
|
38
|
+
assert.ok(registration);
|
|
39
|
+
assert.match(readFileSync(join(registration.path, "reviewer.md"), "utf8"), /Packaged reviewer role/);
|
|
40
|
+
|
|
41
|
+
const resolved = await loadingRegistry().resolveModelAliases({
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
projectTrusted: true,
|
|
44
|
+
rootModel: { provider: "example", model: "root" },
|
|
45
|
+
knownModels: new Set(["example/root", "example/available"]),
|
|
46
|
+
availableModels: new Set(["example/available"]),
|
|
47
|
+
signal: new AbortController().signal,
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(resolved, { "template-model": "example/available" });
|
|
50
|
+
|
|
51
|
+
const hook = loadingRegistry().agentSetupHooks().find(({ name }) => name === "templateAdvisor");
|
|
52
|
+
assert.ok(hook);
|
|
53
|
+
const agent = { prompt: "Review this", options: {}, sessionInput: {} };
|
|
54
|
+
await hook.setup(agent, { signal: new AbortController().signal });
|
|
55
|
+
assert.equal(agent.sessionInput.systemPromptAppend, undefined);
|
|
56
|
+
agent.options.templateAdvisor = true;
|
|
57
|
+
await hook.setup(agent, { signal: new AbortController().signal });
|
|
58
|
+
assert.match(agent.sessionInput.systemPromptAppend, /one concrete risk/);
|
|
59
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { registerWorkflowExtension } from "pi-extensible-workflows";
|
|
2
|
+
|
|
3
|
+
const templateExtension = {
|
|
4
|
+
version: "1.0.0",
|
|
5
|
+
headline: "Workflow extension template",
|
|
6
|
+
description: "A copyable registered function with a packaged role.",
|
|
7
|
+
functions: {
|
|
8
|
+
greet: {
|
|
9
|
+
description: "Return a greeting for one person.",
|
|
10
|
+
input: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { name: { type: "string" } },
|
|
13
|
+
required: ["name"],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
output: { type: "string" },
|
|
17
|
+
run(input) {
|
|
18
|
+
return `Hello, ${String(input.name)}!`;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// Packaged resources must be absolute paths or file URLs. This URL stays
|
|
23
|
+
// correct when the extension is copied or installed elsewhere.
|
|
24
|
+
roleDirectories: [new URL("./roles/", import.meta.url)],
|
|
25
|
+
// Optional advanced example: select an available model without naming a
|
|
26
|
+
// provider-specific model in the extension.
|
|
27
|
+
modelAliases: {
|
|
28
|
+
"template-model": {
|
|
29
|
+
resolve({ availableModels, rootModel }) {
|
|
30
|
+
const root = `${rootModel.provider}/${rootModel.model}`;
|
|
31
|
+
return availableModels.has(root) ? root : availableModels.values().next().value ?? root;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// Optional advanced example: mutate setup only for explicitly opted-in
|
|
36
|
+
// agents. Setup hooks are trusted code and should stay narrowly scoped.
|
|
37
|
+
agentSetupHooks: {
|
|
38
|
+
templateAdvisor: {
|
|
39
|
+
setup(agent, context) {
|
|
40
|
+
if (context.signal.aborted || agent.options.templateAdvisor !== true) return;
|
|
41
|
+
const suffix = "\n\nAdvisor: call out one concrete risk and one next check.";
|
|
42
|
+
const existing = agent.sessionInput.systemPromptAppend;
|
|
43
|
+
agent.sessionInput.systemPromptAppend = existing ? existing + suffix : suffix;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function extension() {
|
|
50
|
+
registerWorkflowExtension(templateExtension);
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"src",
|
|
26
26
|
"skills",
|
|
27
27
|
"evals",
|
|
28
|
+
"examples",
|
|
28
29
|
"test/fixtures"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"inspect": "node dist/src/cli.js inspect",
|
|
33
34
|
"lint": "eslint .",
|
|
34
35
|
"test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
|
|
35
|
-
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test $TEST_FILES",
|
|
36
|
+
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test --test-concurrency=1 $TEST_FILES",
|
|
36
37
|
"acceptance": "npm run build && TEST_FILES='dist/test/runtime-acceptance.test.js' npm run test:run",
|
|
37
38
|
"docs:check": "node scripts/check-docs.mjs",
|
|
38
39
|
"check": "npm run lint && npm test && npm run docs:check",
|
|
@@ -5,47 +5,36 @@ description: Use when the task is complex enough to require multiple subagents o
|
|
|
5
5
|
|
|
6
6
|
# pi-extensible-workflows
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Default path
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use `workflow` only for genuinely multi-agent orchestration; a single agent uses ordinary tools or `Agent` directly. Give phases distinct responsibilities and keep result flow explicit.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const reportSchema = {
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {
|
|
16
|
-
summary: { type: "string" },
|
|
17
|
-
findings: { type: "array", items: { type: "string" } },
|
|
18
|
-
},
|
|
19
|
-
required: ["summary", "findings"],
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
};
|
|
12
|
+
For most multi-agent tasks, start with a named inline workflow: provide a non-empty `name` and a `script` that fans out independent work with `parallel(...)`, awaits the keyed results, passes them into one summarizing `agent(...)`, and returns.
|
|
22
13
|
|
|
14
|
+
```js
|
|
23
15
|
const reports = await parallel("research", {
|
|
24
|
-
first: () =>
|
|
25
|
-
|
|
26
|
-
role: "scout",
|
|
27
|
-
outputSchema: reportSchema,
|
|
28
|
-
}),
|
|
29
|
-
second: () =>
|
|
30
|
-
agent("Research the second target.", {
|
|
31
|
-
role: "scout",
|
|
32
|
-
outputSchema: reportSchema,
|
|
33
|
-
}),
|
|
16
|
+
first: () => agent("Research the first target."),
|
|
17
|
+
second: () => agent("Research the second target."),
|
|
34
18
|
});
|
|
35
19
|
|
|
36
|
-
return agent(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
20
|
+
return await agent(
|
|
21
|
+
prompt("Summarize these reports:\n\n{reports}", { reports }),
|
|
22
|
+
);
|
|
40
23
|
```
|
|
41
24
|
|
|
25
|
+
Await `parallel(...)` or `pipeline(...)` results before interpolation. Runs are backgrounded by default; set the tool-call `foreground: true` when the caller must wait for the final value.
|
|
26
|
+
|
|
27
|
+
## Runtime and safety rules
|
|
28
|
+
|
|
29
|
+
|
|
42
30
|
Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
|
|
43
31
|
|
|
44
32
|
```json
|
|
45
33
|
{ "workflow": "workflowName", "args": { "issue": 42 } }
|
|
46
34
|
```
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId })` replays a failed run into a child; `workflow_resume({ runId, budget? })` continues a `budget_exhausted` run; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
|
|
36
|
+
For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
|
|
37
|
+
Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
|
|
49
38
|
Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
|
|
50
39
|
|
|
51
40
|
Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
|
|
@@ -63,6 +52,10 @@ if (testRes.exitCode === 0) {
|
|
|
63
52
|
|
|
64
53
|
Most of the times using `shell()` to perform mutations is an antipattern. Use it mainly for verifications or idempotent actions.
|
|
65
54
|
|
|
55
|
+
## Advanced capabilities
|
|
56
|
+
|
|
57
|
+
Registered functions, `outputSchema`, budgets, checkpoints, worktrees, retry/resume, CLI export, and `pipeline(...)` remain available for workflows that need them. Treat these as advanced controls rather than requirements for the default inline path.
|
|
58
|
+
|
|
66
59
|
## `agent()` options
|
|
67
60
|
|
|
68
61
|
```typescript
|
package/src/execution.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fork, spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -78,6 +78,7 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
78
78
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
79
79
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
80
80
|
const agentOccurrences = new Map();
|
|
81
|
+
const agentInflight = new Set();
|
|
81
82
|
const shellOccurrences = new Map();
|
|
82
83
|
const worktreeOwners = new AsyncLocalStorage();
|
|
83
84
|
const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
|
|
@@ -98,14 +99,22 @@ const internalAgent = (...values) => {
|
|
|
98
99
|
const callSite = values.pop();
|
|
99
100
|
if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
|
|
100
101
|
const inherited = inheritedAgentPath.getStore() || [];
|
|
101
|
-
// ponytail: same-callsite races outside parallel/pipeline lack a stable structural scope and are unsupported.
|
|
102
102
|
const occurrenceKey = JSON.stringify([inherited, callSite]);
|
|
103
|
+
if (agentInflight.has(occurrenceKey)) throw workError("INVALID_METADATA", "Concurrent agent calls from the same source call site are unsupported; use parallel(...) or pipeline(...)");
|
|
104
|
+
agentInflight.add(occurrenceKey);
|
|
103
105
|
const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
|
|
104
106
|
agentOccurrences.set(occurrenceKey, occurrence);
|
|
105
107
|
const options = values.length < 2 || values[1] === undefined ? {} : values[1];
|
|
106
108
|
const worktreeOwner = worktreeOwners.getStore();
|
|
107
109
|
const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
|
|
108
|
-
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = rpc("agent", [values[0], options, identity]).then(unwrap);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
agentInflight.delete(occurrenceKey);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
void result.then(() => agentInflight.delete(occurrenceKey), () => agentInflight.delete(occurrenceKey));
|
|
109
118
|
Object.defineProperties(result, {
|
|
110
119
|
toJSON: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before serialization"); } },
|
|
111
120
|
toString: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
|
|
@@ -386,7 +395,7 @@ function workflowErrorFromWorker(error: WorkerErrorShape): WorkflowError {
|
|
|
386
395
|
export function runWorkflow(script: string, args: JsonValue = null, bridge: WorkflowBridge = {}, signal?: AbortSignal): WorkflowExecution {
|
|
387
396
|
encoded(args);
|
|
388
397
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
389
|
-
const childDir = mkdtempSync(join(tmpdir(), "pi-wf-"));
|
|
398
|
+
const childDir = realpathSync(mkdtempSync(join(tmpdir(), "pi-wf-")));
|
|
390
399
|
const childFile = join(childDir, "child.cjs");
|
|
391
400
|
writeFileSync(childFile, childSource);
|
|
392
401
|
const child: ChildProcess = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
|