@parcha/agentrun-pi 0.1.0-beta.11 → 0.1.0-beta.13
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 +20 -8
- package/dist/cli.js +10 -1
- package/dist/demo-cli.d.ts +2 -0
- package/dist/demo-cli.js +25 -0
- package/dist/durable/decision-recovery.d.ts +23 -0
- package/dist/durable/decision-recovery.js +57 -0
- package/dist/durable/decisions.d.ts +113 -0
- package/dist/durable/decisions.js +71 -0
- package/dist/durable/index.d.ts +19 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/node-runner.d.ts +100 -0
- package/dist/durable/node-runner.js +243 -0
- package/dist/durable/node.d.ts +88 -0
- package/dist/durable/node.js +81 -0
- package/dist/durable/pi-versions.d.ts +34 -0
- package/dist/durable/pi-versions.js +66 -0
- package/dist/durable/readers.d.ts +25 -0
- package/dist/durable/readers.js +64 -0
- package/dist/durable/receipts.d.ts +52 -0
- package/dist/durable/receipts.js +91 -0
- package/dist/durable/record-file.d.ts +10 -0
- package/dist/durable/record-file.js +56 -0
- package/dist/durable/record-tool.d.ts +76 -0
- package/dist/durable/record-tool.js +123 -0
- package/dist/durable/record.d.ts +157 -0
- package/dist/durable/record.js +402 -0
- package/dist/durable/recording.d.ts +90 -0
- package/dist/durable/recording.js +292 -0
- package/dist/durable/store.d.ts +23 -0
- package/dist/durable/store.js +192 -0
- package/dist/durable/task.d.ts +129 -0
- package/dist/durable/task.js +395 -0
- package/dist/extension-service.js +14 -1
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +98 -13
- package/dist/host-session.js +3 -1
- package/dist/run-error.d.ts +6 -0
- package/dist/run-error.js +13 -0
- package/dist/runner.d.ts +1 -6
- package/dist/runner.js +2 -12
- package/dist/sop.d.ts +12 -0
- package/dist/sop.js +29 -0
- package/dist/tool-context.d.ts +21 -0
- package/dist/tool-context.js +13 -0
- package/dist/workflow-session.d.ts +4 -0
- package/dist/workflow-session.js +3 -2
- package/package.json +43 -8
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# agent.run() + Pi
|
|
2
2
|
|
|
3
|
-
Describe a task in Pi, inspect the workflow, run it with your current model, and save its definition for new input. Requires Node 22.19+ and Pi
|
|
3
|
+
Describe a task in Pi, inspect the workflow, run it with your current model, and save its definition for new input. Requires Node 22.19+ and Pi 1.1.0.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
If you do not have Pi, install the tested host version first:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install -g @earendil-works/pi-coding-agent@
|
|
10
|
+
npm install -g @earendil-works/pi-coding-agent@1.1.0
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Then, in your project:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
pi install npm:@parcha/agentrun-pi@
|
|
16
|
+
pi install npm:@parcha/agentrun-pi@beta -l
|
|
17
17
|
pi --offline
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -29,7 +29,15 @@ Try the bundled example inside Pi:
|
|
|
29
29
|
/agentrun demo
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
No Pi session at hand? The same scripted workflow runs from a shell, with no key and no network:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx @parcha/agentrun-pi@beta demo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It prints each sub-question with its answer, then `Status: complete`, the answer and the scripted call counts, and exits 0. `npx @parcha/agentrun-pi@beta demo empty` runs the missing-evidence path and prints `Status: escalated`.
|
|
39
|
+
|
|
40
|
+
The demo shows the workflow graph, progress, and a completed result with call counts. It uses scripted responses over fictional sources. It makes no model calls and does not test model quality. Run `/agentrun status` to check the loaded skill and Pi host version. The tested version is 1.1.0; status flags a different host without blocking it. Missing Pi or Jev access does not prevent the scripted demo. To repeat it, run `/agentrun run` or `/agentrun demo`. A scripted demo stays scripted on rerun, including its missing-evidence path. Use `/agentrun demo live` to switch to real Pi and Jev calls. Workflows authored by Pi use real adapters.
|
|
33
41
|
|
|
34
42
|
For an offline Pi startup, use `pi --offline`. Without that flag, Pi may download optional command-line tools on first launch. This is separate from the scripted demo, which makes no network calls.
|
|
35
43
|
|
|
@@ -75,7 +83,9 @@ Loading starts with empty input and does not execute. A save retains the definit
|
|
|
75
83
|
|
|
76
84
|
A progress widget shows active steps and the number finished. Results distinguish completion, escalation, failure, and interruption. Decision records show supplied evidence, criteria, answer, and route, not hidden reasoning or proof of correctness.
|
|
77
85
|
|
|
78
|
-
Pi
|
|
86
|
+
Pi 1.1.0 cannot export a fresh session containing only slash-command results. To save an offline research receipt from the checkout, run `node examples/research-live.mjs --out research-result.json`. This runs the scripted example again and writes its result; it does not export the Pi session.
|
|
87
|
+
|
|
88
|
+
A live run is durable. Its journal is kept in `.pi/agentrun/runs/<run id>/journal.json`, and `/agentrun run` after an interruption (`/agentrun stop`, a closed session, a killed process) or a failure continues the same run: committed steps are answered from the journal, a tool call that completed is answered from its receipt and is not made again, and the next step runs. The run is bound to its workflow, input, SOP text and tool names; a change to any of them starts a new run. A tool call that was in flight when the run stopped has an unknown outcome and is never made again: the resumed run fails naming it, and the person who knows whether it happened reconciles it before a new run. An agent step that was interrupted starts over from its first turn. A run that completed or escalated is not resumed; `/agentrun run` starts a new one. Scripted demos keep no journal.
|
|
79
89
|
|
|
80
90
|
One workflow runs at a time. Named definitions are durable across sessions in the same project. Pi run receipts belong to the current branch and may remain in memory until Pi persists its session; saving a definition does not save a receipt.
|
|
81
91
|
|
|
@@ -95,7 +105,7 @@ By default, the extension exposes currently active Pi built-in `read`, `grep`, `
|
|
|
95
105
|
|
|
96
106
|
The user must issue `/agentrun run --trusted` for each run that needs code nodes or active built-in `bash`, `edit`, or `write` tools. This permits local, unsandboxed execution; authorization does not carry into the next run. A model cannot grant itself that permission with a tool argument.
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
A workflow that names `sopSection`s needs SOP text. `/agentrun sop <file>` reads a Markdown file inside the project (up to 512 KiB) and checks that it has a `## <section>` heading for each section the workflow names; `/agentrun sop` shows what is set. The file's digest is kept with the session, and a run is refused if the file changed since it was supplied. A host that passes `sop` to `createAgentRunExtension` supplies the text itself and `/agentrun sop` is not used.
|
|
99
109
|
|
|
100
110
|
Custom extension tools and the outer Pi session's permission hooks are **not inherited**, including in trusted runs. A configured host can explicitly supply its own tool definitions as described below. Shell/executor effect transports and artifact delivery are unavailable in this extension; declared tool effects must use an available tool.
|
|
101
111
|
|
|
@@ -240,9 +250,11 @@ These commands create a separate `agentrun-pi-project` beside the checkout. You
|
|
|
240
250
|
|
|
241
251
|
## Dependencies and retained data
|
|
242
252
|
|
|
243
|
-
Pi is optional for `@parcha/agentrun-dsl`. This package
|
|
253
|
+
Pi is optional for `@parcha/agentrun-dsl`. This package depends on the DSL and the Jev adapter only. The Pi packages it uses (`@earendil-works/pi-coding-agent`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-tui`) and `typebox` are optional peer dependencies at `*`, as Pi's package guidance requires, so an installed copy never shadows the host's. The release inventory records the locked versions.
|
|
254
|
+
|
|
255
|
+
When loaded as an extension, Pi resolves its core packages and TypeBox through the host loader, and `pi install` adds nothing else. A standalone application or the `agentrun-pi` CLI has no host: install the peers yourself (`npm install @earendil-works/pi-coding-agent @earendil-works/pi-agent-core @earendil-works/pi-tui typebox`). The tested host is Pi 1.1.0; peer ranges at `*` do not make an older or newer host compatible. The offline loader regression checks competing local dependencies, a native file read, and the scripted workflow. Pi's [package guidance](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md#dependencies) is the rule this follows.
|
|
244
256
|
|
|
245
|
-
The
|
|
257
|
+
The durable entry (`@parcha/agentrun-pi/durable`) takes pi-durable and chord as optional peers from 1.0.4 to below 1.3; CI runs it on 1.0.4, 1.1.0 and 1.2.0. A run that pi-durable 1.1 left (killed, paused, or with a tool call in flight) continues on 1.2. The move does not go back. A run that pi 1.2 left with a tool call in flight cannot be continued by pi 1.1: 1.1 waits on the newer task, and the only thing it writes is that task's mark from running back to pending, as pi does for any task a dead process left running. Continue such a run on 1.2, and do not downgrade pi under a live run. A host can call `refuseNewerTasks(harness, context)` from the durable entry after opening its Harness. On pi before 1.2, it then fails such a store with `NewerTaskError` (code `PI_TASK_NEWER`, naming the task's kind and version) instead of waiting.
|
|
246
258
|
|
|
247
259
|
Candidate directories retain request text, rubric text, workflow versions, and feedback. Native Pi messages can contain workflow inputs and results. Choose storage and tool access appropriate for that data. The author skill in `@parcha/agentrun-dsl` describes the native and SDK authoring paths for agents.
|
|
248
260
|
|
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,14 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { authorWorkflow, runWorkflow, validateWorkflow } from "@parcha/agentrun-dsl";
|
|
6
|
-
import {
|
|
6
|
+
import { runScriptedDemo } from "./demo-cli.js";
|
|
7
7
|
const help = `Usage:
|
|
8
|
+
agentrun-pi demo [empty] scripted demo; no key, no model calls, no network
|
|
8
9
|
agentrun-pi validate workflow.json [--inputs key1,key2]
|
|
9
10
|
agentrun-pi author --config ./pi.config.mjs --out ./candidates "Describe the workflow"
|
|
10
11
|
agentrun-pi run --config ./pi.config.mjs workflow.json input.json
|
|
11
12
|
|
|
13
|
+
The demo runs the same scripted workflow as /agentrun demo inside Pi; "empty" runs its missing-evidence path and exits 0 on the expected escalation.
|
|
12
14
|
Config is trusted JavaScript exporting default PiRunnerOptions, optionally deps (Jev/effect adapters).
|
|
13
15
|
Author saves candidates; it does not execute or activate them.
|
|
14
16
|
Validate may execute code probes. Run executes trusted workflow code and exits 2 on escalation.
|
|
@@ -29,6 +31,12 @@ async function main(args) {
|
|
|
29
31
|
args.splice(index, 2);
|
|
30
32
|
return value;
|
|
31
33
|
};
|
|
34
|
+
if (command === "demo") {
|
|
35
|
+
if (args.length > 1 || (args[0] && args[0] !== "empty"))
|
|
36
|
+
throw new Error("Usage: agentrun-pi demo [empty]");
|
|
37
|
+
process.exitCode = await runScriptedDemo(args[0] === "empty", line => console.log(line));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
32
40
|
const configPath = flag("--config");
|
|
33
41
|
const outputDir = flag("--out") ?? "./candidates";
|
|
34
42
|
const inputKeys = (flag("--inputs") ?? "").split(",").filter(Boolean);
|
|
@@ -46,6 +54,7 @@ async function main(args) {
|
|
|
46
54
|
throw new Error(help);
|
|
47
55
|
if (!configPath)
|
|
48
56
|
throw new Error("Supply --config with an explicit model and modelRuntime");
|
|
57
|
+
const { createPiRunner } = await import("./runner.js");
|
|
49
58
|
const config = await import(pathToFileURL(resolve(configPath)).href);
|
|
50
59
|
if (command === "author") {
|
|
51
60
|
if (args.length !== 1)
|
package/dist/demo-cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { runWorkflow } from "@parcha/agentrun-dsl";
|
|
2
|
+
import { demoInput, demoWorkflow, scriptedDemoDeps } from "./demo.js";
|
|
3
|
+
/** The bundled scripted workflow, run through the interpreter with fixed answers: no model, no key, no network. */
|
|
4
|
+
export async function runScriptedDemo(noEvidence, print) {
|
|
5
|
+
const scripted = scriptedDemoDeps(noEvidence);
|
|
6
|
+
const calls = { effects: 0, judgments: 0, agentSteps: 0 };
|
|
7
|
+
const deps = {
|
|
8
|
+
runEffect: input => { calls.effects++; return scripted.runEffect(input); },
|
|
9
|
+
runJudge: input => { calls.judgments++; return scripted.runJudge(input); },
|
|
10
|
+
runNode: input => { calls.agentSteps++; return scripted.runNode(input); },
|
|
11
|
+
};
|
|
12
|
+
print("AgentRun on Pi · scripted demo, fictional sources, no model calls\n");
|
|
13
|
+
print(`Question: ${demoInput.question}\n`);
|
|
14
|
+
const result = await runWorkflow(demoWorkflow, demoInput, deps);
|
|
15
|
+
const state = result.state;
|
|
16
|
+
for (const finding of state.findings ?? [])
|
|
17
|
+
print(` ${finding.question}\n ${finding.answer}`);
|
|
18
|
+
print(`\nStatus: ${result.status}`);
|
|
19
|
+
if (result.status === "complete")
|
|
20
|
+
print(`Answer: ${state.report?.answer ?? ""}`);
|
|
21
|
+
else
|
|
22
|
+
print(`Escalation: ${result.escalation?.summary ?? ""}`);
|
|
23
|
+
print(`Calls: ${calls.effects} tool, ${calls.judgments} judgment, ${calls.agentSteps} agent step (all scripted)`);
|
|
24
|
+
return result.status === "escalated" && !noEvidence ? 2 : 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DecisionExchange, type DecisionReceipt } from "@parcha/agentrun-dsl";
|
|
2
|
+
import type { DocumentReader } from "@earendil-works/pi-durable";
|
|
3
|
+
import type { WorkflowInvocation } from "./task.js";
|
|
4
|
+
type Context = WorkflowInvocation["context"];
|
|
5
|
+
/** The interpreter's two decision hooks over one invocation of a workflow task. */
|
|
6
|
+
export type DecisionRecovery = {
|
|
7
|
+
/** The interpreter's `recordDecision`: commits the receipt's row after every row recorded before it, and resolves
|
|
8
|
+
* once that commit is durable; a commit that fails rejects this receipt's call. The task's state is untouched. */
|
|
9
|
+
recordDecision(receipt: DecisionReceipt, exchange: DecisionExchange): Promise<void>;
|
|
10
|
+
/** The interpreter's `recovery.decision`: the receipt rebuilt from the last answered row this run recorded under
|
|
11
|
+
* `id`, or undefined when it recorded none (a request that failed, one recorded without its identity, or one another
|
|
12
|
+
* run recorded, is asked again). */
|
|
13
|
+
decision(id: string): Promise<DecisionReceipt | undefined>;
|
|
14
|
+
};
|
|
15
|
+
/** The decision hooks a durable host gives the interpreter inside `WorkflowTaskHost.run`, over the invocation it is
|
|
16
|
+
* handed (the run's key, the task's runtime and context): `recordDecision` as the interpreter's own, and `decision` as
|
|
17
|
+
* `recovery.decision` beside the driver's recovery hooks. */
|
|
18
|
+
export declare function decisionRecovery(invocation: {
|
|
19
|
+
key: string;
|
|
20
|
+
runtime: Pick<WorkflowInvocation["runtime"], "commit"> & DocumentReader;
|
|
21
|
+
context: Context;
|
|
22
|
+
}): DecisionRecovery;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A workflow task's decision receipts, durable in its run's Session: each receipt is a row (decisions.ts) committed
|
|
2
|
+
// through the task's own commit before the interpreter acts on its answers, and a resumed run's request is answered
|
|
3
|
+
// from the row its earlier process committed, so the judge is not asked again and the run decides what it decided.
|
|
4
|
+
// Every run in the Session shares the rows' document, and a receipt's id does not name its run, so each row carries
|
|
5
|
+
// the run's key and a run is answered only from its own rows.
|
|
6
|
+
import { DECISION_RECEIPT_VERSION } from "@parcha/agentrun-dsl";
|
|
7
|
+
import { appendDecision, DECISION_ROW_VERSION, readDecisions } from "./decisions.js";
|
|
8
|
+
/** A receipt as the row the run `key` stores: the receipt's own fields, its identity, and the request's size. */
|
|
9
|
+
function rowOf(receipt, exchange, key) {
|
|
10
|
+
return {
|
|
11
|
+
v: DECISION_ROW_VERSION, at: receipt.at, node: receipt.node, kind: receipt.kind, item: receipt.item, execution_path: receipt.execution_path,
|
|
12
|
+
request_sha256: receipt.request_sha256,
|
|
13
|
+
request_bytes: Buffer.byteLength(JSON.stringify({ state: exchange.request.state, questions: exchange.request.questions })),
|
|
14
|
+
model: receipt.model, questions: receipt.questions, answers: receipt.answers, rule: receipt.rule, action: receipt.action,
|
|
15
|
+
usage: receipt.usage, cost_usd: receipt.cost_usd, latency_ms: receipt.latency_ms, ...(receipt.error !== undefined ? { error: receipt.error } : {}),
|
|
16
|
+
decision_id: receipt.id, request: receipt.request, state_sha256: receipt.state_sha256, run_key: key,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The receipt the run `key`'s last answered row under `id` is: a rule is set only on an answered receipt, and an
|
|
20
|
+
* answered one carries no error. */
|
|
21
|
+
function answeredReceipt(rows, id, key) {
|
|
22
|
+
const row = rows.findLast((stored) => stored.run_key === key && stored.decision_id === id && stored.rule !== null && stored.error === undefined
|
|
23
|
+
&& stored.request !== undefined && stored.state_sha256 !== undefined && stored.execution_path !== null);
|
|
24
|
+
if (!row)
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
v: DECISION_RECEIPT_VERSION, id, at: row.at, node: row.node, kind: row.kind, item: row.item, execution_path: row.execution_path,
|
|
28
|
+
request: row.request, status: "answered", state_sha256: row.state_sha256,
|
|
29
|
+
questions: row.questions, answers: row.answers, rule: row.rule, action: row.action,
|
|
30
|
+
model: row.model, usage: row.usage, cost_usd: row.cost_usd, request_sha256: row.request_sha256, latency_ms: row.latency_ms,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The decision hooks a durable host gives the interpreter inside `WorkflowTaskHost.run`, over the invocation it is
|
|
34
|
+
* handed (the run's key, the task's runtime and context): `recordDecision` as the interpreter's own, and `decision` as
|
|
35
|
+
* `recovery.decision` beside the driver's recovery hooks. */
|
|
36
|
+
export function decisionRecovery(invocation) {
|
|
37
|
+
const { key, runtime, context } = invocation;
|
|
38
|
+
const harness = {
|
|
39
|
+
commit: async (change) => {
|
|
40
|
+
let result;
|
|
41
|
+
await runtime.commit(async (tx) => { result = await change(tx); return undefined; }, context);
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
let chain = Promise.resolve();
|
|
46
|
+
return {
|
|
47
|
+
recordDecision(receipt, exchange) {
|
|
48
|
+
const row = rowOf(receipt, exchange, key);
|
|
49
|
+
const committed = chain.then(() => appendDecision(harness, row, context));
|
|
50
|
+
chain = committed.catch(() => undefined);
|
|
51
|
+
return committed;
|
|
52
|
+
},
|
|
53
|
+
async decision(id) {
|
|
54
|
+
return answeredReceipt(await readDecisions(runtime, context), id, key);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/chord";
|
|
2
|
+
import { type DocumentReader, type Harness } from "@earendil-works/pi-durable";
|
|
3
|
+
/** The session document that holds a run's decision rows. */
|
|
4
|
+
export declare const DECISION_RECEIPTS_DOC = "agentrun.decisions";
|
|
5
|
+
/** The stored row's version. */
|
|
6
|
+
export declare const DECISION_ROW_VERSION = 1;
|
|
7
|
+
/** How the answers became the action, with every threshold applied. */
|
|
8
|
+
export type DecisionRowRule =
|
|
9
|
+
/** judge: each question's decoded value (choice, noul at 0.5, rounded score). */
|
|
10
|
+
{
|
|
11
|
+
kind: "judge";
|
|
12
|
+
}
|
|
13
|
+
/** pick: the chosen option names an item by its position, or none. */
|
|
14
|
+
| {
|
|
15
|
+
kind: "pick";
|
|
16
|
+
options: string[];
|
|
17
|
+
none: string | null;
|
|
18
|
+
}
|
|
19
|
+
/** route: the chosen branch, or the unsure branch when the choice's confidence is below `gte`. */
|
|
20
|
+
| {
|
|
21
|
+
kind: "route";
|
|
22
|
+
unsure: {
|
|
23
|
+
gte: number;
|
|
24
|
+
branch: string;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
/** sift: an item is kept when its `keep` measure meets `gte`; every item without a keep rule. */
|
|
28
|
+
| {
|
|
29
|
+
kind: "sift";
|
|
30
|
+
items: number;
|
|
31
|
+
ids: string[];
|
|
32
|
+
keep: {
|
|
33
|
+
id: string;
|
|
34
|
+
measure: "confidence" | "value";
|
|
35
|
+
gte: number;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
/** ask: the predicate holds when p_yes meets `gte`. */
|
|
39
|
+
| {
|
|
40
|
+
kind: "ask";
|
|
41
|
+
gte: number;
|
|
42
|
+
}
|
|
43
|
+
/** verify: a noul question fails when p_yes is below its threshold (null: not judged, an empty field); a failed
|
|
44
|
+
* question the submission holds is doubted, a failed one it lacks is unmet. */
|
|
45
|
+
| {
|
|
46
|
+
kind: "verify";
|
|
47
|
+
thresholds: Record<string, number | null>;
|
|
48
|
+
present: string[];
|
|
49
|
+
};
|
|
50
|
+
/** One stored decision row. */
|
|
51
|
+
export type DecisionRow = {
|
|
52
|
+
v: typeof DECISION_ROW_VERSION;
|
|
53
|
+
at: string;
|
|
54
|
+
node: string;
|
|
55
|
+
kind: "judge" | "pick" | "sift" | "route" | "ask" | "verify";
|
|
56
|
+
item: number | null;
|
|
57
|
+
execution_path: string | null;
|
|
58
|
+
request_sha256: string | null;
|
|
59
|
+
request_bytes: number;
|
|
60
|
+
model: string | null;
|
|
61
|
+
questions: Record<string, unknown>;
|
|
62
|
+
answers: Record<string, unknown>;
|
|
63
|
+
rule: DecisionRowRule | null;
|
|
64
|
+
/** What the run did with the answers; null when the node did not act on them. */
|
|
65
|
+
action: unknown;
|
|
66
|
+
usage: {
|
|
67
|
+
input_tokens: number;
|
|
68
|
+
output_tokens: number;
|
|
69
|
+
} | null;
|
|
70
|
+
cost_usd: number | null;
|
|
71
|
+
latency_ms: number;
|
|
72
|
+
error?: string;
|
|
73
|
+
/** The request's spend is on its frozen driver's state, so a cost fold counts it there, not here. */
|
|
74
|
+
driver_spend?: true;
|
|
75
|
+
/** The receipt's id, its place among its node's requests, the digest of the state it was asked with, and the key of
|
|
76
|
+
* the workflow run that asked it. A row that carries all four is the whole receipt again, so that run, resumed, can be
|
|
77
|
+
* answered from it (`decisionRecovery`); another run in the same Session never is. */
|
|
78
|
+
decision_id?: string;
|
|
79
|
+
request?: {
|
|
80
|
+
index: number;
|
|
81
|
+
count: number;
|
|
82
|
+
items?: number[];
|
|
83
|
+
};
|
|
84
|
+
state_sha256?: string;
|
|
85
|
+
run_key?: string;
|
|
86
|
+
};
|
|
87
|
+
/** Commit one row, appended after every row committed before it. */
|
|
88
|
+
export declare function appendDecision(harness: Pick<Harness, "commit">, row: DecisionRow, context?: Context): Promise<void>;
|
|
89
|
+
/** Every row the run's Session holds, in commit order. */
|
|
90
|
+
export declare function readDecisions(read: DocumentReader, context?: Context): Promise<DecisionRow[]>;
|
|
91
|
+
/** Where a writer commits: the run's Session, opened when the first row needs it and released by `flush`. */
|
|
92
|
+
export type DecisionSession = {
|
|
93
|
+
harness: Pick<Harness, "commit">;
|
|
94
|
+
release(): Promise<void>;
|
|
95
|
+
};
|
|
96
|
+
export type DecisionWriter = {
|
|
97
|
+
/** Commit `row` after every row recorded before it. It returns at once; a commit that fails is kept for
|
|
98
|
+
* `settled` and `flush`, and the rows after it are still committed. */
|
|
99
|
+
record(row: DecisionRow): void;
|
|
100
|
+
/** Every row recorded so far is committed; the first that could not be is thrown, and kept for `flush`. The
|
|
101
|
+
* Session stays open. */
|
|
102
|
+
settled(): Promise<void>;
|
|
103
|
+
/** Every row is committed and every Session the writer opened is released, also when a commit failed; a row
|
|
104
|
+
* recorded while flush runs is committed too. Then the first failed commit is thrown, or, when none failed, the
|
|
105
|
+
* first failed release. */
|
|
106
|
+
flush(): Promise<void>;
|
|
107
|
+
};
|
|
108
|
+
/** A writer that commits rows in the order they are recorded. `open` runs once, when the first row is committed, so
|
|
109
|
+
* a run that records no row never opens its Session here. */
|
|
110
|
+
export declare function createDecisionWriter(options: {
|
|
111
|
+
open: () => Promise<DecisionSession>;
|
|
112
|
+
context?: Context;
|
|
113
|
+
}): DecisionWriter;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// A run's decision rows as pi-durable stores them: the session document `agentrun.decisions`, one row per typed
|
|
2
|
+
// question the run asked, in commit order. The row is a stored format (version 1) and does not change with the
|
|
3
|
+
// interpreter: a host maps `@parcha/agentrun-dsl`'s `DecisionReceipt` to a row, with the facts only the host has (the
|
|
4
|
+
// request's size and metering, whether the node acted on the answers), and this module stores and reads rows without
|
|
5
|
+
// looking inside them.
|
|
6
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
7
|
+
import { defineDoc } from "@earendil-works/pi-durable";
|
|
8
|
+
/** The session document that holds a run's decision rows. */
|
|
9
|
+
export const DECISION_RECEIPTS_DOC = "agentrun.decisions";
|
|
10
|
+
/** The stored row's version. */
|
|
11
|
+
export const DECISION_ROW_VERSION = 1;
|
|
12
|
+
const DecisionsDoc = defineDoc({ kind: DECISION_RECEIPTS_DOC, version: 1, scope: "session", initial: () => ({ receipts: [] }) });
|
|
13
|
+
/** Commit one row, appended after every row committed before it. */
|
|
14
|
+
export async function appendDecision(harness, row, context = BACKGROUND_CONTEXT) {
|
|
15
|
+
const stored = JSON.parse(JSON.stringify(row));
|
|
16
|
+
await harness.commit(async (tx) => { (await tx.doc(DecisionsDoc)).receipts.push(stored); }, context);
|
|
17
|
+
}
|
|
18
|
+
/** Every row the run's Session holds, in commit order. */
|
|
19
|
+
export async function readDecisions(read, context = BACKGROUND_CONTEXT) {
|
|
20
|
+
const doc = await read.snapshot(DecisionsDoc, context);
|
|
21
|
+
return (doc?.receipts ?? []);
|
|
22
|
+
}
|
|
23
|
+
/** A writer that commits rows in the order they are recorded. `open` runs once, when the first row is committed, so
|
|
24
|
+
* a run that records no row never opens its Session here. */
|
|
25
|
+
export function createDecisionWriter(options) {
|
|
26
|
+
let session;
|
|
27
|
+
let chain = Promise.resolve();
|
|
28
|
+
let failure;
|
|
29
|
+
return {
|
|
30
|
+
record(row) {
|
|
31
|
+
const stored = JSON.parse(JSON.stringify(row));
|
|
32
|
+
chain = chain
|
|
33
|
+
.then(async () => appendDecision((await (session ??= options.open())).harness, stored, options.context))
|
|
34
|
+
.catch((error) => { failure ??= { error }; });
|
|
35
|
+
},
|
|
36
|
+
async settled() {
|
|
37
|
+
await chain;
|
|
38
|
+
if (failure)
|
|
39
|
+
throw failure.error;
|
|
40
|
+
},
|
|
41
|
+
async flush() {
|
|
42
|
+
let released;
|
|
43
|
+
// A row recorded while flush waits or releases joins the chain, so flush returns only when a full pass saw no new
|
|
44
|
+
// row. The Session is cleared before it is released: a row recorded meanwhile opens its own, which the next pass
|
|
45
|
+
// releases.
|
|
46
|
+
for (;;) {
|
|
47
|
+
const drained = chain;
|
|
48
|
+
await drained;
|
|
49
|
+
if (drained !== chain)
|
|
50
|
+
continue;
|
|
51
|
+
const held = session;
|
|
52
|
+
session = undefined;
|
|
53
|
+
if (held) {
|
|
54
|
+
const opened = await held.catch(() => undefined);
|
|
55
|
+
try {
|
|
56
|
+
await opened?.release();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
released ??= { error };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (drained === chain)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
const first = failure ?? released;
|
|
66
|
+
failure = undefined;
|
|
67
|
+
if (first)
|
|
68
|
+
throw first.error;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { RecoveryBinding, RecoveryEffect, RecoveryJournal, RecoveryNote, RecoveryStore } from "@parcha/agentrun-dsl/recovery";
|
|
2
|
+
export { documentStore, type DocumentStoreHost, type DocumentStoreOptions } from "./store.js";
|
|
3
|
+
export { committedRoutes, durableDoc, readJournal, type JournalView } from "./readers.js";
|
|
4
|
+
export { recordCommits, recordingLines, snapshotLines, type RecordingLine, type RecordingSource } from "./recording.js";
|
|
5
|
+
export { reuseReceipts, seedReceipts, ReceiptIndex, type ReceiptReuse, type ReuseReceiptsOptions, type ReusableTool } from "./receipts.js";
|
|
6
|
+
export { NewerTaskError, refuseNewerTasks, toolResult, ToolResultKeyError, type TextBlock } from "./pi-versions.js";
|
|
7
|
+
export { DECISION_RECEIPTS_DOC, DECISION_ROW_VERSION, appendDecision, createDecisionWriter, readDecisions } from "./decisions.js";
|
|
8
|
+
export type { DecisionRow, DecisionRowRule, DecisionSession, DecisionWriter } from "./decisions.js";
|
|
9
|
+
export { decisionRecovery, type DecisionRecovery } from "./decision-recovery.js";
|
|
10
|
+
export { DELIVERY_ATTEMPTS, deliver, deliveryEnded, deliveryTerminates, deliveryText, disagreementText, extrasNotInRecord, fileSubmission, gate, healNullSpellings, lintRecordRoot, lintRecordSchema, mixedFileSubmission, nudgeText, nullSpellings, parseStringifiedContainers, repairRecord, spend, submitFooter, VIOLATIONS_SHOWN, } from "./record.js";
|
|
11
|
+
export type { Delivery, DeliveryCount, DeliveryEvent, Disagreement, GateState, Reading, RecordContract, RecordState, RecordStore, Refusal, Reviewer } from "./record.js";
|
|
12
|
+
export { workspaceRecordFile } from "./record-file.js";
|
|
13
|
+
export { deliveredRecord, GateDoc, RecordDoc, recordNudge, recordStore, recordTool, spendNudge, submitDefinition } from "./record-tool.js";
|
|
14
|
+
export type { RecordNudgeOptions, RecordToolOptions } from "./record-tool.js";
|
|
15
|
+
export { closeNodeAttempt, hostScope, NodeBindingMismatch, NodeIndex, runNodeAttempt, taskScope } from "./node.js";
|
|
16
|
+
export type { NodeAttempt, NodeOutcome, NodeScope, NodeSettlement } from "./node.js";
|
|
17
|
+
export { NodeDoc, NodeFailure, nodeRunner } from "./node-runner.js";
|
|
18
|
+
export type { NodeHost, NodeRecord, NodeRequest } from "./node-runner.js";
|
|
19
|
+
export { createWorkflowExtension, type WorkflowEnding, type WorkflowHostParts, type WorkflowInvocation, type WorkflowOutcome, type WorkflowRequest, type WorkflowRun, type WorkflowStop, type WorkflowTaskHost } from "./task.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { documentStore } from "./store.js";
|
|
2
|
+
export { committedRoutes, durableDoc, readJournal } from "./readers.js";
|
|
3
|
+
export { recordCommits, recordingLines, snapshotLines } from "./recording.js";
|
|
4
|
+
export { reuseReceipts, seedReceipts, ReceiptIndex } from "./receipts.js";
|
|
5
|
+
export { NewerTaskError, refuseNewerTasks, toolResult, ToolResultKeyError } from "./pi-versions.js";
|
|
6
|
+
export { DECISION_RECEIPTS_DOC, DECISION_ROW_VERSION, appendDecision, createDecisionWriter, readDecisions } from "./decisions.js";
|
|
7
|
+
export { decisionRecovery } from "./decision-recovery.js";
|
|
8
|
+
export { DELIVERY_ATTEMPTS, deliver, deliveryEnded, deliveryTerminates, deliveryText, disagreementText, extrasNotInRecord, fileSubmission, gate, healNullSpellings, lintRecordRoot, lintRecordSchema, mixedFileSubmission, nudgeText, nullSpellings, parseStringifiedContainers, repairRecord, spend, submitFooter, VIOLATIONS_SHOWN, } from "./record.js";
|
|
9
|
+
export { workspaceRecordFile } from "./record-file.js";
|
|
10
|
+
export { deliveredRecord, GateDoc, RecordDoc, recordNudge, recordStore, recordTool, spendNudge, submitDefinition } from "./record-tool.js";
|
|
11
|
+
export { closeNodeAttempt, hostScope, NodeBindingMismatch, NodeIndex, runNodeAttempt, taskScope } from "./node.js";
|
|
12
|
+
export { NodeDoc, NodeFailure, nodeRunner } from "./node-runner.js";
|
|
13
|
+
export { createWorkflowExtension } from "./task.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Context, JsonValue } from "@earendil-works/chord";
|
|
2
|
+
import { type AgentChange, type ConversationId, type Extension, type Tx } from "@earendil-works/pi-durable";
|
|
3
|
+
import type { WorkflowDeps } from "@parcha/agentrun-dsl";
|
|
4
|
+
import { type RecoveryNodeParams } from "@parcha/agentrun-dsl/recovery";
|
|
5
|
+
import { type NodeScope } from "./node.js";
|
|
6
|
+
import { type Disagreement, type RecordContract } from "./record.js";
|
|
7
|
+
import { type RecordToolOptions } from "./record-tool.js";
|
|
8
|
+
/** A node's configuration: what its conversation's prompt and `submit` tool are drawn from. */
|
|
9
|
+
export declare const NodeDoc: import("@earendil-works/pi-durable").ConversationDocToken<{
|
|
10
|
+
system: string;
|
|
11
|
+
label: string;
|
|
12
|
+
schema: JsonValue | null;
|
|
13
|
+
fileKey: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
/** A node as the host is asked about it: the interpreter's request, and the attempt it runs as, with the sessions of
|
|
16
|
+
* the attempts before it (a host that refuses to repeat a call an earlier attempt left unknown reads them). */
|
|
17
|
+
export type NodeRequest = Omit<Parameters<NonNullable<WorkflowDeps["runNode"]>>[0], "review" | "signal"> & {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
attempt: number;
|
|
20
|
+
earlierSessionIds: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
/** What a host says of a node's record beside its schema. */
|
|
23
|
+
export type NodeRecord = Pick<RecordContract, "contracts" | "checks"> & {
|
|
24
|
+
/** What the model is told it delivers (default: `the <label> record`). */
|
|
25
|
+
label?: string;
|
|
26
|
+
/** The host's own delivery instructions, said with the contract that ends the node's task. */
|
|
27
|
+
instructions?: string;
|
|
28
|
+
/** The record may be delivered as a JSON file the node wrote in `workspace`, named under `key`. */
|
|
29
|
+
file?: {
|
|
30
|
+
key: string;
|
|
31
|
+
workspace: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type NodeHost = {
|
|
35
|
+
/** The agent a node's conversation runs with: its model, thinking level, working directory and the host's own
|
|
36
|
+
* extensions and tools. The runner adds its extension and `submit` where the host did not place them. */
|
|
37
|
+
agent(node: NodeRequest): AgentChange | Promise<AgentChange>;
|
|
38
|
+
/** Duty text for a node's system prompt, after the interpreter's. `offered` names the tools the agent lists. */
|
|
39
|
+
stance?(node: NodeRequest, offered: {
|
|
40
|
+
tools: readonly string[];
|
|
41
|
+
}): string | undefined;
|
|
42
|
+
record?(node: NodeRequest): NodeRecord | Promise<NodeRecord>;
|
|
43
|
+
/** A second reading of a node's record, before the node's own verify clause: what it disagrees with. */
|
|
44
|
+
verify?(record: unknown, node: NodeRequest, context: {
|
|
45
|
+
round: boolean;
|
|
46
|
+
}): Disagreement[] | Promise<Disagreement[]>;
|
|
47
|
+
/** Called, and awaited, when a node's conversation exists and before any request of this process reaches a model:
|
|
48
|
+
* the host attaches its observers to that conversation. A throw fails the node's call and releases nothing: what
|
|
49
|
+
* pi resumed of the node waits for the attempt's next entry, or ends with the Harness's close. */
|
|
50
|
+
onNodeOpen?(node: NodeRequest & {
|
|
51
|
+
conversationId: ConversationId;
|
|
52
|
+
resumed: boolean;
|
|
53
|
+
}): void | Promise<void>;
|
|
54
|
+
/** Written in the commit that creates a node's conversation, after the runner's own document: the documents the
|
|
55
|
+
* host's extensions read for it (a plane's configuration). */
|
|
56
|
+
init?(node: NodeRequest, tx: Tx, conversationId: ConversationId): void | Promise<void>;
|
|
57
|
+
/** Delivery attempts a node gets (default `DELIVERY_ATTEMPTS`). */
|
|
58
|
+
maxAttempts?: number;
|
|
59
|
+
/** The host's part in every `submit` call: its rows, and which errors are its own failure. The one delivery it may
|
|
60
|
+
* be told before `onNodeOpen` is `already`, the answer of a call that a process died inside after its commit. */
|
|
61
|
+
submit?: Pick<RecordToolOptions, "onDelivery" | "fatal" | "onFatal" | "fatalText">;
|
|
62
|
+
/** The digest an attempt is bound by. Absent, one of the node's label, path, kind, item and attempt. */
|
|
63
|
+
digest?(node: NodeRequest): string;
|
|
64
|
+
};
|
|
65
|
+
/** A node that delivered no record. `final` says a second attempt would spend the same again: the model did not
|
|
66
|
+
* answer, or the delivery attempts are spent. `unanswered` is pi's own account of a submission the model did not
|
|
67
|
+
* answer: its reason (`failed`, `aborted`, ...) and detail. */
|
|
68
|
+
export declare class NodeFailure extends Error {
|
|
69
|
+
readonly node: string;
|
|
70
|
+
readonly final: boolean;
|
|
71
|
+
readonly executionPath?: string | undefined;
|
|
72
|
+
readonly unanswered?: {
|
|
73
|
+
reason: string;
|
|
74
|
+
detail?: JsonValue;
|
|
75
|
+
} | undefined;
|
|
76
|
+
readonly code = "NODE_NOT_DELIVERED";
|
|
77
|
+
constructor(message: string, node: string, final: boolean, executionPath?: string | undefined, unanswered?: {
|
|
78
|
+
reason: string;
|
|
79
|
+
detail?: JsonValue;
|
|
80
|
+
} | undefined);
|
|
81
|
+
}
|
|
82
|
+
/** The node runner over a host. Install `extension` in the registry before the Harness opens; `on` binds the runner
|
|
83
|
+
* to where its conversations live (`taskScope`, `hostScope`). */
|
|
84
|
+
export declare function nodeRunner(host: NodeHost, options?: {
|
|
85
|
+
name?: string;
|
|
86
|
+
}): {
|
|
87
|
+
extension: Extension;
|
|
88
|
+
/** The extension a conversation an older build opened still selects, under that build's name for it: the system
|
|
89
|
+
* text that build kept as the `task` section, and this runner's `submit`. Each of its requests and calls waits for
|
|
90
|
+
* the runner to reach the node, which then moves the conversation to `extension`. Install one for each node an
|
|
91
|
+
* older build left unfinished, before the Harness resumes. */
|
|
92
|
+
legacy(entry: {
|
|
93
|
+
extension: string;
|
|
94
|
+
system: string | null;
|
|
95
|
+
}): Extension;
|
|
96
|
+
on(scope: NodeScope, context: Context): {
|
|
97
|
+
runNode(params: RecoveryNodeParams): Promise<unknown>;
|
|
98
|
+
closeStepSession(sessionId: string): Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
};
|