@osolmaz/pi-workflows 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/extension/executor.d.ts +58 -0
- package/dist/extension/executor.js +201 -0
- package/dist/extension/executor.js.map +1 -0
- package/dist/extension/index.d.ts +17 -0
- package/dist/extension/index.js +504 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/widget.d.ts +21 -0
- package/dist/extension/widget.js +142 -0
- package/dist/extension/widget.js.map +1 -0
- package/dist/render/ansi.d.ts +16 -0
- package/dist/render/ansi.js +42 -0
- package/dist/render/ansi.js.map +1 -0
- package/dist/render/canvas.d.ts +40 -0
- package/dist/render/canvas.js +177 -0
- package/dist/render/canvas.js.map +1 -0
- package/dist/render/format.d.ts +3 -0
- package/dist/render/format.js +17 -0
- package/dist/render/format.js.map +1 -0
- package/dist/render/graph-render.d.ts +22 -0
- package/dist/render/graph-render.js +520 -0
- package/dist/render/graph-render.js.map +1 -0
- package/dist/render/graph.d.ts +46 -0
- package/dist/render/graph.js +272 -0
- package/dist/render/graph.js.map +1 -0
- package/dist/viewer/cli.d.ts +10 -0
- package/dist/viewer/cli.js +132 -0
- package/dist/viewer/cli.js.map +1 -0
- package/dist/viewer/render.d.ts +19 -0
- package/dist/viewer/render.js +162 -0
- package/dist/viewer/render.js.map +1 -0
- package/dist/viewer/tui.d.ts +11 -0
- package/dist/viewer/tui.js +140 -0
- package/dist/viewer/tui.js.map +1 -0
- package/dist/viewer/watch.d.ts +9 -0
- package/dist/viewer/watch.js +46 -0
- package/dist/viewer/watch.js.map +1 -0
- package/dist/workflows/decision.d.ts +25 -0
- package/dist/workflows/decision.js +96 -0
- package/dist/workflows/decision.js.map +1 -0
- package/dist/workflows/definition.d.ts +9 -0
- package/dist/workflows/definition.js +61 -0
- package/dist/workflows/definition.js.map +1 -0
- package/dist/workflows/engine.d.ts +65 -0
- package/dist/workflows/engine.js +574 -0
- package/dist/workflows/engine.js.map +1 -0
- package/dist/workflows/errors.d.ts +9 -0
- package/dist/workflows/errors.js +24 -0
- package/dist/workflows/errors.js.map +1 -0
- package/dist/workflows/graph.d.ts +17 -0
- package/dist/workflows/graph.js +127 -0
- package/dist/workflows/graph.js.map +1 -0
- package/dist/workflows/index.d.ts +11 -0
- package/dist/workflows/index.js +11 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/json.d.ts +14 -0
- package/dist/workflows/json.js +134 -0
- package/dist/workflows/json.js.map +1 -0
- package/dist/workflows/loader.d.ts +28 -0
- package/dist/workflows/loader.js +94 -0
- package/dist/workflows/loader.js.map +1 -0
- package/dist/workflows/schema.d.ts +7 -0
- package/dist/workflows/schema.js +176 -0
- package/dist/workflows/schema.js.map +1 -0
- package/dist/workflows/shell.d.ts +9 -0
- package/dist/workflows/shell.js +177 -0
- package/dist/workflows/shell.js.map +1 -0
- package/dist/workflows/store.d.ts +35 -0
- package/dist/workflows/store.js +181 -0
- package/dist/workflows/store.js.map +1 -0
- package/dist/workflows/text.d.ts +10 -0
- package/dist/workflows/text.js +32 -0
- package/dist/workflows/text.js.map +1 -0
- package/dist/workflows/types.d.ts +280 -0
- package/dist/workflows/types.js +2 -0
- package/dist/workflows/types.js.map +1 -0
- package/docs/development.md +130 -0
- package/docs/run-bundles.md +114 -0
- package/docs/workflows.md +311 -0
- package/examples/workflows/autoimplement.workflow.ts +92 -0
- package/examples/workflows/autoresearch.workflow.ts +139 -0
- package/examples/workflows/branch.workflow.ts +63 -0
- package/examples/workflows/echo.workflow.ts +23 -0
- package/examples/workflows/elegant-solution.workflow.ts +95 -0
- package/examples/workflows/shell.workflow.ts +31 -0
- package/examples/workflows/two-turn.workflow.ts +64 -0
- package/package.json +80 -0
- package/src/extension/executor.ts +251 -0
- package/src/extension/index.ts +627 -0
- package/src/extension/widget.ts +183 -0
- package/src/render/ansi.ts +47 -0
- package/src/render/canvas.ts +196 -0
- package/src/render/format.ts +19 -0
- package/src/render/graph-render.ts +738 -0
- package/src/render/graph.ts +341 -0
- package/src/viewer/cli.ts +150 -0
- package/src/viewer/render.ts +236 -0
- package/src/viewer/tui.ts +159 -0
- package/src/viewer/watch.ts +55 -0
- package/src/workflows/decision.ts +127 -0
- package/src/workflows/definition.ts +104 -0
- package/src/workflows/engine.ts +793 -0
- package/src/workflows/errors.ts +27 -0
- package/src/workflows/graph.ts +161 -0
- package/src/workflows/index.ts +76 -0
- package/src/workflows/json.ts +155 -0
- package/src/workflows/loader.ts +123 -0
- package/src/workflows/schema.ts +218 -0
- package/src/workflows/shell.ts +199 -0
- package/src/workflows/store.ts +234 -0
- package/src/workflows/text.ts +34 -0
- package/src/workflows/types.ts +318 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { agent, checkpoint, decision, decisionEdge, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
const sameChoices = ["y", "n"] as const;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trigger this mid-conversation with /workflow elegant-solution once a
|
|
7
|
+
* problem has been discussed. It makes the model commit to the most elegant
|
|
8
|
+
* long-term production-ready solution, articulate the holy grail, and check
|
|
9
|
+
* whether the two match before implementing.
|
|
10
|
+
*/
|
|
11
|
+
export default defineWorkflow({
|
|
12
|
+
name: "elegant-solution",
|
|
13
|
+
title: "Elegant production-ready solution",
|
|
14
|
+
presentationPrompt: ({ state }) =>
|
|
15
|
+
state.status === "waiting"
|
|
16
|
+
? [
|
|
17
|
+
"Explain the elegant proposal, the holy grail, the important gap, and the pragmatic path in plain language.",
|
|
18
|
+
"End by asking the user whether to implement the pragmatic path.",
|
|
19
|
+
`Proposal: ${JSON.stringify(state.outputs.propose)}`,
|
|
20
|
+
`Holy grail: ${JSON.stringify(state.outputs.holy_grail)}`,
|
|
21
|
+
].join("\n")
|
|
22
|
+
: "Summarize what was implemented, what was tested, and what still needs verification.",
|
|
23
|
+
startAt: "propose",
|
|
24
|
+
nodes: {
|
|
25
|
+
propose: agent({
|
|
26
|
+
prompt: () =>
|
|
27
|
+
[
|
|
28
|
+
"Consider the problem discussed so far in this conversation.",
|
|
29
|
+
"What is the most elegant and long-term production-ready solution?",
|
|
30
|
+
"Think it through and commit to one concrete proposal.",
|
|
31
|
+
].join("\n"),
|
|
32
|
+
expectedOutput: `{ "solution": "the proposed solution", "rationale": "why it is the most elegant long-term choice" }`,
|
|
33
|
+
}),
|
|
34
|
+
holy_grail: agent({
|
|
35
|
+
prompt: () =>
|
|
36
|
+
[
|
|
37
|
+
"Now set your previous proposal aside for a moment.",
|
|
38
|
+
"What is the holy grail for this problem?",
|
|
39
|
+
"Describe the ideal end state with no constraints on effort.",
|
|
40
|
+
].join("\n"),
|
|
41
|
+
expectedOutput: `{ "holyGrail": "the ideal end state" }`,
|
|
42
|
+
}),
|
|
43
|
+
compare: decision({
|
|
44
|
+
choices: sameChoices,
|
|
45
|
+
question: ({ outputs }) =>
|
|
46
|
+
[
|
|
47
|
+
"Is the holy grail the same as what you proposed? Answer y or n.",
|
|
48
|
+
"",
|
|
49
|
+
`Proposal: ${JSON.stringify(outputs.propose)}`,
|
|
50
|
+
`Holy grail: ${JSON.stringify(outputs.holy_grail)}`,
|
|
51
|
+
].join("\n"),
|
|
52
|
+
}),
|
|
53
|
+
implement: agent({
|
|
54
|
+
timeoutMs: 60 * 60_000,
|
|
55
|
+
statusDetail: "implementing",
|
|
56
|
+
prompt: ({ outputs }) =>
|
|
57
|
+
[
|
|
58
|
+
"The proposal matches the holy grail, so implement it now.",
|
|
59
|
+
"Implement the most elegant and long-term production-ready solution end-to-end.",
|
|
60
|
+
"Test as much as possible without destructive actions, and state what could not be verified locally.",
|
|
61
|
+
"",
|
|
62
|
+
`Proposal: ${JSON.stringify(outputs.propose)}`,
|
|
63
|
+
].join("\n"),
|
|
64
|
+
expectedOutput: `{ "implemented": true, "summary": "what was built", "tested": "what was verified", "untested": "what still needs verification" }`,
|
|
65
|
+
}),
|
|
66
|
+
reconcile: agent({
|
|
67
|
+
prompt: ({ outputs }) =>
|
|
68
|
+
[
|
|
69
|
+
"The proposal and the holy grail differ.",
|
|
70
|
+
"Explain the gap and propose the pragmatic path: what to build now and how it evolves toward the holy grail.",
|
|
71
|
+
"",
|
|
72
|
+
`Proposal: ${JSON.stringify(outputs.propose)}`,
|
|
73
|
+
`Holy grail: ${JSON.stringify(outputs.holy_grail)}`,
|
|
74
|
+
].join("\n"),
|
|
75
|
+
expectedOutput: `{ "gap": "how they differ", "path": "what to build now and how it evolves" }`,
|
|
76
|
+
}),
|
|
77
|
+
review: checkpoint({
|
|
78
|
+
summary: "human decides between proposal and holy grail path",
|
|
79
|
+
run: ({ outputs }) => outputs.reconcile,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
edges: [
|
|
83
|
+
{ from: "propose", to: "holy_grail" },
|
|
84
|
+
{ from: "holy_grail", to: "compare" },
|
|
85
|
+
decisionEdge({
|
|
86
|
+
from: "compare",
|
|
87
|
+
choices: sameChoices,
|
|
88
|
+
cases: {
|
|
89
|
+
y: "implement",
|
|
90
|
+
n: "reconcile",
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
{ from: "reconcile", to: "review" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { compute, defineWorkflow, shell } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type ShellInput = {
|
|
4
|
+
text?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** One runtime-owned shell action returning structured JSON, no agent step. */
|
|
8
|
+
export default defineWorkflow({
|
|
9
|
+
name: "shell",
|
|
10
|
+
startAt: "echo_text",
|
|
11
|
+
nodes: {
|
|
12
|
+
echo_text: shell({
|
|
13
|
+
exec: ({ input }) => ({
|
|
14
|
+
command: "printf",
|
|
15
|
+
args: ["%s", (input as ShellInput).text ?? "hello from pi-workflows"],
|
|
16
|
+
timeoutMs: 10_000,
|
|
17
|
+
}),
|
|
18
|
+
parse: (result) => ({
|
|
19
|
+
stdout: result.stdout,
|
|
20
|
+
exitCode: result.exitCode,
|
|
21
|
+
durationMs: result.durationMs,
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
finalize: compute({
|
|
25
|
+
run: ({ outputs }) => ({
|
|
26
|
+
echoed: (outputs.echo_text as { stdout: string }).stdout,
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
edges: [{ from: "echo_text", to: "finalize" }],
|
|
31
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { agent, compute, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type TwoTurnInput = {
|
|
4
|
+
topic?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Multi-step agent work in the same pi conversation. Each step builds on the
|
|
9
|
+
* previous step's accepted output, and the model keeps its full session
|
|
10
|
+
* context (files it read, tools it ran) across steps.
|
|
11
|
+
*/
|
|
12
|
+
export default defineWorkflow({
|
|
13
|
+
name: "two-turn",
|
|
14
|
+
presentationPrompt:
|
|
15
|
+
"Present the final validation checklist clearly, including its concrete file references.",
|
|
16
|
+
startAt: "inspect",
|
|
17
|
+
nodes: {
|
|
18
|
+
inspect: agent({
|
|
19
|
+
prompt: ({ input }) => {
|
|
20
|
+
const topic =
|
|
21
|
+
(input as TwoTurnInput).topic ??
|
|
22
|
+
"How should we validate a new extension before shipping it?";
|
|
23
|
+
return [
|
|
24
|
+
"Inspect the current workspace before answering.",
|
|
25
|
+
"Read at least two files relevant to the topic (for example the package manifest and a source file).",
|
|
26
|
+
"",
|
|
27
|
+
`Topic: ${topic}`,
|
|
28
|
+
].join("\n");
|
|
29
|
+
},
|
|
30
|
+
expectedOutput: `{ "findings": ["short finding", "short finding"], "repoSummary": "short paragraph" }`,
|
|
31
|
+
}),
|
|
32
|
+
draft: agent({
|
|
33
|
+
prompt: ({ outputs }) =>
|
|
34
|
+
[
|
|
35
|
+
"Build on your earlier inspection in this same conversation.",
|
|
36
|
+
"Write a short draft answer about the topic.",
|
|
37
|
+
"",
|
|
38
|
+
`Inspection: ${JSON.stringify(outputs.inspect)}`,
|
|
39
|
+
].join("\n"),
|
|
40
|
+
expectedOutput: `{ "draft": "short paragraph" }`,
|
|
41
|
+
}),
|
|
42
|
+
checklist: agent({
|
|
43
|
+
prompt: ({ outputs }) =>
|
|
44
|
+
[
|
|
45
|
+
"Turn the draft into a concise validation checklist with concrete references.",
|
|
46
|
+
"",
|
|
47
|
+
`Draft: ${JSON.stringify(outputs.draft)}`,
|
|
48
|
+
].join("\n"),
|
|
49
|
+
expectedOutput: `{ "checklist": ["item", "item"], "references": ["path", "path"] }`,
|
|
50
|
+
}),
|
|
51
|
+
finalize: compute({
|
|
52
|
+
run: ({ outputs }) => ({
|
|
53
|
+
inspection: outputs.inspect,
|
|
54
|
+
draft: outputs.draft,
|
|
55
|
+
checklist: outputs.checklist,
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
edges: [
|
|
60
|
+
{ from: "inspect", to: "draft" },
|
|
61
|
+
{ from: "draft", to: "checklist" },
|
|
62
|
+
{ from: "checklist", to: "finalize" },
|
|
63
|
+
],
|
|
64
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@osolmaz/pi-workflows",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Workflow engine, JSON control-flow tool, and live terminal viewer for the pi coding agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/osolmaz/pi-workflows#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/osolmaz/pi-workflows/issues"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/osolmaz/pi-workflows.git"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"pi-workflows": "dist/viewer/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"examples",
|
|
24
|
+
"docs",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/workflows/index.d.ts",
|
|
32
|
+
"default": "./dist/workflows/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./extension": {
|
|
35
|
+
"types": "./dist/extension/index.d.ts",
|
|
36
|
+
"default": "./dist/extension/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"check": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test:coverage",
|
|
45
|
+
"format": "oxfmt --write",
|
|
46
|
+
"format:check": "oxfmt --check",
|
|
47
|
+
"lint": "oxlint --deny-warnings src test examples",
|
|
48
|
+
"prepare": "node scripts/prepare.mjs",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"jiti": "^2.7.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
59
|
+
"@types/node": "^26.1.1",
|
|
60
|
+
"@vitest/coverage-istanbul": "^4.1.10",
|
|
61
|
+
"oxfmt": "^0.59.0",
|
|
62
|
+
"oxlint": "^1.74.0",
|
|
63
|
+
"tsx": "^4.23.1",
|
|
64
|
+
"typebox": "^1.3.6",
|
|
65
|
+
"typescript": "^7.0.2",
|
|
66
|
+
"vitest": "^4.1.10"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
70
|
+
"typebox": "*"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=22"
|
|
74
|
+
},
|
|
75
|
+
"pi": {
|
|
76
|
+
"extensions": [
|
|
77
|
+
"./src/extension/index.ts"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentStepExecutor,
|
|
3
|
+
AgentStepRequest,
|
|
4
|
+
AgentStepSubmission,
|
|
5
|
+
} from "../workflows/types.js";
|
|
6
|
+
|
|
7
|
+
export type SubmissionResult =
|
|
8
|
+
| { accepted: true; message: string }
|
|
9
|
+
| { accepted: false; message: string };
|
|
10
|
+
|
|
11
|
+
export type PromptDelivery = {
|
|
12
|
+
prompt: string;
|
|
13
|
+
/** True when the agent is known to be mid-run, so delivery must be queued. */
|
|
14
|
+
streaming: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ConversationStepExecutorOptions = {
|
|
18
|
+
/** Deliver a prompt into the pi conversation. */
|
|
19
|
+
sendPrompt: (delivery: PromptDelivery) => void;
|
|
20
|
+
/** Reminders sent when the agent settles without submitting. Default 2. */
|
|
21
|
+
maxNudges?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type PendingStep = {
|
|
25
|
+
request: AgentStepRequest;
|
|
26
|
+
resolve: (submission: AgentStepSubmission) => void;
|
|
27
|
+
reject: (error: unknown) => void;
|
|
28
|
+
nudgesSent: number;
|
|
29
|
+
cleanup: () => void;
|
|
30
|
+
/** Resolves when this step stops being the pending step. */
|
|
31
|
+
cleared: Promise<void>;
|
|
32
|
+
markCleared: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MAX_NUDGES = 2;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* AgentStepExecutor that runs steps inside the current pi conversation. The
|
|
39
|
+
* engine hands it a prompt; it delivers the prompt as a user message and
|
|
40
|
+
* resolves once the model submits an accepted output through the `workflow`
|
|
41
|
+
* tool. If the agent settles without submitting, it nudges the model a
|
|
42
|
+
* bounded number of times before failing the step.
|
|
43
|
+
*/
|
|
44
|
+
export class ConversationStepExecutor implements AgentStepExecutor {
|
|
45
|
+
private readonly sendPrompt: (delivery: PromptDelivery) => void;
|
|
46
|
+
private readonly maxNudges: number;
|
|
47
|
+
private pending: PendingStep | null = null;
|
|
48
|
+
private streaming = false;
|
|
49
|
+
private heldByUser = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: ConversationStepExecutorOptions) {
|
|
52
|
+
this.sendPrompt = options.sendPrompt;
|
|
53
|
+
this.maxNudges = options.maxNudges ?? DEFAULT_MAX_NUDGES;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Track agent streaming state (wire to agent_start / agent_settled). */
|
|
57
|
+
setStreaming(streaming: boolean): void {
|
|
58
|
+
this.streaming = streaming;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get pendingStepId(): string | null {
|
|
62
|
+
return this.pending?.request.contract.nodeId ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Hold the pending step for the user: no nudges are sent while held, so an
|
|
67
|
+
* escape-interrupted conversation stays quiet until the user resumes.
|
|
68
|
+
*/
|
|
69
|
+
hold(): void {
|
|
70
|
+
this.heldByUser = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get held(): boolean {
|
|
74
|
+
return this.heldByUser;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Release a user hold. When a step is still pending, its prompt is
|
|
79
|
+
* re-delivered so the model picks the step back up.
|
|
80
|
+
*/
|
|
81
|
+
release(): void {
|
|
82
|
+
if (!this.heldByUser) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.heldByUser = false;
|
|
86
|
+
const pending = this.pending;
|
|
87
|
+
if (!pending) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
this.sendPrompt({ prompt: pending.request.prompt, streaming: this.streaming });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.clearPending();
|
|
94
|
+
pending.reject(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission> {
|
|
99
|
+
if (this.pending) {
|
|
100
|
+
throw new Error("Another workflow step is already awaiting output");
|
|
101
|
+
}
|
|
102
|
+
return await new Promise<AgentStepSubmission>((resolve, reject) => {
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
const reason: unknown = signal.reason ?? new Error("Workflow step aborted");
|
|
105
|
+
this.clearPending();
|
|
106
|
+
reject(reason);
|
|
107
|
+
};
|
|
108
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
109
|
+
let markCleared!: () => void;
|
|
110
|
+
const cleared = new Promise<void>((resolveCleared) => {
|
|
111
|
+
markCleared = resolveCleared;
|
|
112
|
+
});
|
|
113
|
+
this.pending = {
|
|
114
|
+
request,
|
|
115
|
+
resolve,
|
|
116
|
+
reject,
|
|
117
|
+
nudgesSent: 0,
|
|
118
|
+
cleanup: () => signal.removeEventListener("abort", onAbort),
|
|
119
|
+
cleared,
|
|
120
|
+
markCleared,
|
|
121
|
+
};
|
|
122
|
+
if (signal.aborted) {
|
|
123
|
+
onAbort();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
this.sendPrompt({ prompt: request.prompt, streaming: this.streaming });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// A failed delivery must not leave the step installed, or every
|
|
130
|
+
// subsequent agent node would fail with "already awaiting output".
|
|
131
|
+
this.clearPending();
|
|
132
|
+
reject(error);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Called by the `workflow` tool when the model submits a step output. */
|
|
138
|
+
async submit(stepId: string, attemptId: string, output: unknown): Promise<SubmissionResult> {
|
|
139
|
+
const pending = this.pending;
|
|
140
|
+
if (!pending) {
|
|
141
|
+
return {
|
|
142
|
+
accepted: false,
|
|
143
|
+
message:
|
|
144
|
+
"No workflow step is awaiting output. Do not call the workflow tool outside an active workflow step.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const expected = pending.request.contract.nodeId;
|
|
148
|
+
if (stepId !== expected) {
|
|
149
|
+
return {
|
|
150
|
+
accepted: false,
|
|
151
|
+
message: `Wrong step id ${JSON.stringify(stepId)}; the pending step is ${JSON.stringify(expected)}.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Loops revisit the same node id, so a delayed duplicate submission from
|
|
155
|
+
// an earlier attempt would otherwise be accepted as this attempt's output.
|
|
156
|
+
const expectedAttempt = pending.request.contract.attemptId;
|
|
157
|
+
if (attemptId !== expectedAttempt) {
|
|
158
|
+
return {
|
|
159
|
+
accepted: false,
|
|
160
|
+
message: `Stale attempt id ${JSON.stringify(attemptId)} for step ${JSON.stringify(
|
|
161
|
+
stepId,
|
|
162
|
+
)}; the pending attempt is ${JSON.stringify(expectedAttempt)}. Use the attempt id from the latest step contract.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Race validation against the step being cleared: a hung `validate`
|
|
166
|
+
// callback must not leave this tool call (and therefore pi) blocked after
|
|
167
|
+
// a timeout or cancel already resolved the run.
|
|
168
|
+
const result = await Promise.race([
|
|
169
|
+
pending.request.accept(output),
|
|
170
|
+
pending.cleared.then(() => null),
|
|
171
|
+
]);
|
|
172
|
+
// The step may have timed out or been cancelled (and a newer step
|
|
173
|
+
// installed) while validation was awaited; a stale submission must not
|
|
174
|
+
// clear or resolve the newer pending step.
|
|
175
|
+
if (result === null || this.pending !== pending) {
|
|
176
|
+
return {
|
|
177
|
+
accepted: false,
|
|
178
|
+
message: `Step ${JSON.stringify(stepId)} is no longer awaiting output.`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
return {
|
|
183
|
+
accepted: false,
|
|
184
|
+
message: `Output rejected for step ${JSON.stringify(stepId)}: ${result.error}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
this.clearPending();
|
|
188
|
+
pending.resolve({ output: result.value });
|
|
189
|
+
return {
|
|
190
|
+
accepted: true,
|
|
191
|
+
message: [
|
|
192
|
+
`Output accepted for step ${JSON.stringify(stepId)}.`,
|
|
193
|
+
"If the workflow continues, the next step arrives as a new user message. End your turn now.",
|
|
194
|
+
].join(" "),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Called when the agent settles. Returns true when a nudge was sent, false
|
|
200
|
+
* when there was nothing to do. Fails the pending step once the nudge
|
|
201
|
+
* budget is exhausted.
|
|
202
|
+
*/
|
|
203
|
+
handleAgentSettled(): boolean {
|
|
204
|
+
const pending = this.pending;
|
|
205
|
+
if (!pending) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (this.heldByUser) {
|
|
209
|
+
// The user interrupted deliberately; reminding the model now would
|
|
210
|
+
// steal the conversation back. The step waits for an explicit resume.
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (pending.nudgesSent >= this.maxNudges) {
|
|
214
|
+
this.clearPending();
|
|
215
|
+
pending.reject(
|
|
216
|
+
new Error(
|
|
217
|
+
`Agent settled ${pending.nudgesSent + 1} times without submitting step ${JSON.stringify(
|
|
218
|
+
pending.request.contract.nodeId,
|
|
219
|
+
)} via the workflow tool`,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
pending.nudgesSent += 1;
|
|
225
|
+
const { nodeId, attemptId } = pending.request.contract;
|
|
226
|
+
try {
|
|
227
|
+
this.sendPrompt({
|
|
228
|
+
prompt: [
|
|
229
|
+
`Reminder: workflow step ${JSON.stringify(nodeId)} is still awaiting your output.`,
|
|
230
|
+
"Complete it by calling the `workflow` tool with:",
|
|
231
|
+
`{"step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
232
|
+
`Expected output: ${pending.request.contract.expectedOutput ?? "a JSON object with your result"}`,
|
|
233
|
+
].join("\n"),
|
|
234
|
+
streaming: this.streaming,
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// No reminder turn was started, so nothing would settle the step; fail
|
|
238
|
+
// it promptly instead of waiting out the node timeout.
|
|
239
|
+
this.clearPending();
|
|
240
|
+
pending.reject(error);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private clearPending(): void {
|
|
247
|
+
this.pending?.cleanup();
|
|
248
|
+
this.pending?.markCleared();
|
|
249
|
+
this.pending = null;
|
|
250
|
+
}
|
|
251
|
+
}
|