@parcha/agentrun-pi 0.1.0-beta.2 → 0.1.0-beta.20
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 +104 -27
- package/dist/cli.js +15 -4
- package/dist/demo-cli.d.ts +2 -0
- package/dist/demo-cli.js +25 -0
- package/dist/demo.js +1 -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 +20 -0
- package/dist/durable/index.js +14 -0
- package/dist/durable/node-runner.d.ts +106 -0
- package/dist/durable/node-runner.js +253 -0
- package/dist/durable/node.d.ts +88 -0
- package/dist/durable/node.js +98 -0
- package/dist/durable/pi-versions.d.ts +34 -0
- package/dist/durable/pi-versions.js +66 -0
- package/dist/durable/readers.d.ts +28 -0
- package/dist/durable/readers.js +103 -0
- package/dist/durable/receipts.d.ts +54 -0
- package/dist/durable/receipts.js +92 -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 +240 -0
- package/dist/durable/task.d.ts +133 -0
- package/dist/durable/task.js +411 -0
- package/dist/durable/workflow-tool.d.ts +32 -0
- package/dist/durable/workflow-tool.js +188 -0
- package/dist/extension-service.js +63 -22
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +469 -80
- package/dist/host-addendum.d.ts +4 -0
- package/dist/host-addendum.js +20 -0
- package/dist/host-session.js +5 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/pi-host.d.ts +7 -0
- package/dist/pi-host.js +22 -0
- package/dist/presentation.js +2 -2
- 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 +7 -13
- package/dist/signin/index.d.ts +9 -0
- package/dist/signin/index.js +7 -0
- package/dist/signin/limit.d.ts +10 -0
- package/dist/signin/limit.js +23 -0
- package/dist/signin/resolve.d.ts +47 -0
- package/dist/signin/resolve.js +163 -0
- package/dist/signin/signin.d.ts +44 -0
- package/dist/signin/signin.js +126 -0
- package/dist/signin/spec.d.ts +116 -0
- package/dist/signin/spec.js +181 -0
- package/dist/signin/store.d.ts +40 -0
- package/dist/signin/store.js +206 -0
- package/dist/skills/author/SKILL.md +138 -0
- package/{skills → dist/skills}/author/references/jev-decisions.md +27 -14
- package/dist/skills/author/references/language.md +134 -0
- package/dist/skills/author/references/sdk-authoring.md +11 -0
- package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
- package/dist/sop.d.ts +12 -0
- package/dist/sop.js +29 -0
- package/dist/thinking.d.ts +3 -0
- package/dist/thinking.js +7 -0
- package/dist/tool-context.d.ts +21 -0
- package/dist/tool-context.js +13 -0
- package/dist/triage-demo.d.ts +62 -0
- package/dist/triage-demo.js +153 -0
- package/dist/types.d.ts +7 -0
- package/dist/workflow-inspector.d.ts +47 -0
- package/dist/workflow-inspector.js +235 -0
- package/dist/workflow-session.d.ts +23 -0
- package/dist/workflow-session.js +168 -0
- package/dist/workflow-store.d.ts +36 -0
- package/dist/workflow-store.js +255 -0
- package/dist/workflow-tool.d.ts +81 -0
- package/dist/workflow-tool.js +94 -0
- package/dist/workflow-view.d.ts +64 -0
- package/dist/workflow-view.js +273 -0
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.js +3 -0
- package/package.json +57 -11
- package/dist/admission.d.ts +0 -6
- package/dist/admission.js +0 -51
- package/dist/author.d.ts +0 -21
- package/dist/author.js +0 -72
- package/dist/benchmark-contract.d.ts +0 -88
- package/dist/benchmark-contract.js +0 -50
- package/dist/benchmark-freeze.d.ts +0 -9
- package/dist/benchmark-freeze.js +0 -9
- package/dist/benchmark.d.ts +0 -127
- package/dist/benchmark.js +0 -260
- package/dist/comparison.d.ts +0 -69
- package/dist/comparison.js +0 -229
- package/dist/skill-bundle.d.ts +0 -5
- package/dist/skill-bundle.js +0 -15
- package/skills/author/SKILL.md +0 -102
- package/skills/author/references/sdk-authoring.md +0 -16
- /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
- /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
- /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
# agent.run() + Pi
|
|
2
2
|
|
|
3
|
-
Describe a task in 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.0.4 to below 1.4 (CI tests 1.0.4, 1.1.0, 1.2.0 and 1.3.0).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
If you do not have Pi, install the
|
|
7
|
+
If you do not have Pi, install it first; 1.3.0 is the newest version CI tests:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install -g @earendil-works/pi-coding-agent@
|
|
10
|
+
npm install -g @earendil-works/pi-coding-agent@1.3.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
|
|
|
20
20
|
`-l` writes project settings in `.pi/settings.json`. Omit it to install for all your Pi sessions. `--offline` skips startup downloads; it does not disable model calls. No AgentRun checkout or build is needed.
|
|
21
21
|
|
|
22
|
-
On first launch, Pi asks whether you trust the project before loading its extension. If Pi is already open, use `/reload`.
|
|
22
|
+
On first launch, Pi asks whether you trust the project before loading its extension. If Pi is already open, use `/reload`. The current draft is restored from a persisted Pi session branch when available; named definition saves are stored separately.
|
|
23
23
|
|
|
24
24
|
For live tasks, configure model access in Pi first. AgentRun uses Pi's current model and authentication; it needs no separate model configuration. The scripted demo below works without provider access.
|
|
25
25
|
|
|
@@ -29,32 +29,79 @@ 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. Status names the supported range, 1.0.4 to below 1.4, and flags a host outside it 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
|
|
|
36
|
-
##
|
|
44
|
+
## Preview, run, save, reuse
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/agentrun Preview a reusable workflow that identifies breaking changes and migration actions in supplied release notes. Do not run it yet.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The command loads the `agentrun-author` skill. Its source ships in `@parcha/agentrun-dsl`; this package registers it rendered with the Pi host rules. A preview validates and displays the graph without running it. Ask Pi to run when ready, or use `/agentrun run`. You can also invoke `/skill:agentrun-author <task>` directly.
|
|
51
|
+
|
|
52
|
+
Open `/agentrun` to inspect the graph in the native terminal UI. Select a stage to see its contract, instructions or criteria, and observed evidence. The Actions page offers run, input, edit, save, load, and history. Arrow keys navigate, Enter inspects, `a` opens Actions, and Escape goes back or closes the view. Closing the inspector does not cancel execution; non-TUI hosts receive a text view.
|
|
53
|
+
|
|
54
|
+
After inspecting a definition, save it and reuse it in a new Pi session in the same project:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
/agentrun input The release note says requests now require X-Workspace.
|
|
58
|
+
/agentrun run
|
|
59
|
+
/agentrun save release-review
|
|
60
|
+
```
|
|
37
61
|
|
|
38
62
|
```text
|
|
39
|
-
/agentrun
|
|
63
|
+
/agentrun load release-review
|
|
64
|
+
/agentrun input The next release removes /events; clients must use /v2/events.
|
|
65
|
+
/agentrun run
|
|
40
66
|
```
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
Loading starts with empty input and does not execute. A save retains the definition, not input or execution permission. Scripted demo adapters are not saved: loading a saved demo uses live adapters and requires the corresponding model and Jev access. Each run restarts with the current input and model; it is not checkpoint resume.
|
|
43
69
|
|
|
44
70
|
| Command | What happens |
|
|
45
71
|
| --- | --- |
|
|
46
|
-
| `/agentrun <task>` | Ask Pi to
|
|
72
|
+
| `/agentrun <task>` | Ask Pi to author, edit, or run; say when you want a preview only. |
|
|
47
73
|
| `/agentrun help` | List commands and demo modes. |
|
|
48
|
-
| `/agentrun` |
|
|
74
|
+
| `/agentrun` | Inspect the current workflow, or show help if there is none. |
|
|
75
|
+
| `/agentrun input <case or JSON>` | Set new input without changing the definition or running it. |
|
|
49
76
|
| `/agentrun run` | Repeat the current workflow with its last supplied input. Scripted demos stay scripted; authored workflows use real adapters. |
|
|
77
|
+
| `/agentrun dry-run` | Run the current workflow on synthetic values, with no model, Jev or tool call. |
|
|
78
|
+
| `/agentrun save <name>` | Save an immutable, project-local definition revision. |
|
|
79
|
+
| `/agentrun load <name> [digest]` | Load the newest or exact revision; input starts empty. |
|
|
80
|
+
| `/agentrun list` | List saved revisions. |
|
|
81
|
+
| `/agentrun history` | Inspect retained run receipts on this Pi branch. |
|
|
50
82
|
| `/agentrun status` | Check skill discovery, active Pi selection, and Jev configuration without making provider calls. |
|
|
51
83
|
| `/agentrun stop` | Request cancellation of the running workflow. |
|
|
52
84
|
|
|
53
|
-
A progress widget shows active steps and the number finished. Results
|
|
85
|
+
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.
|
|
54
86
|
|
|
55
|
-
Pi
|
|
87
|
+
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.
|
|
56
88
|
|
|
57
|
-
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
A host that does not run the extension can save, load and list the same definitions with `@parcha/agentrun-pi/workflows`, which loads nothing from pi:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { WorkflowStore } from '@parcha/agentrun-pi/workflows';
|
|
97
|
+
|
|
98
|
+
const store = new WorkflowStore(projectDirectory); // records go to <projectDirectory>/.pi/agentrun/workflows/<name>/
|
|
99
|
+
const saved = await store.save('release-review', workflow); // an immutable revision, named by its SHA-256
|
|
100
|
+
const again = await store.load('release-review'); // the last one saved; pass a digest for an exact revision
|
|
101
|
+
const revisions = await store.list(); // every revision, newest first
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The argument is a project directory, not the workflows directory: the extension keeps its records for `ctx.cwd` in the same place, so a host that passes that directory and the extension read each other's records. Storage needs real directories and regular files (a symbolic link in the path or among the records fails with `WorkflowStoreError` code `unsafe_path`), and every load checks the record's metadata and the SHA-256 of its workflow. A `<name>/` directory copied into the workflows directory loads. A saved workflow is a procedure, not authority: the host still checks it against its current capabilities before it runs.
|
|
58
105
|
|
|
59
106
|
### Three demo modes
|
|
60
107
|
|
|
@@ -72,7 +119,9 @@ By default, the extension exposes currently active Pi built-in `read`, `grep`, `
|
|
|
72
119
|
|
|
73
120
|
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.
|
|
74
121
|
|
|
75
|
-
|
|
122
|
+
A dry run (`/agentrun dry-run`, the inspector's Dry-run action, or the tool's `dry-run` action, which also takes an `input`) runs the inspected workflow on synthetic values: agent and judge answers and tool results are synthesized from their schemas, so it makes no model, Jev or tool call, writes no run journal and leaves the last run's receipt as it was. Its `status` is `passed`, `failed` (`problems` say why), or `not_run`: a check could not run, for example a schema the generator cannot satisfy (`pattern`, `format`), a synthetic answer that escalated, or SOP text that was not supplied, and `skipped` says which. A `not_run` dry run proves nothing. A workflow with code nodes is `not_run` unless the user issues `/agentrun dry-run --trusted`, which runs its code in this process for that dry run only; a code node that throws is reported by its label, without what it threw. A dry run stops after 10,000 synthetic steps, or at `/agentrun stop`, as `not_run`.
|
|
123
|
+
|
|
124
|
+
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.
|
|
76
125
|
|
|
77
126
|
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.
|
|
78
127
|
|
|
@@ -81,7 +130,7 @@ A direct `call` to a Pi built-in returns a Pi tool result. Its `content` is an a
|
|
|
81
130
|
Jev design guidance is bundled with the author skill and returned by `describe`
|
|
82
131
|
under `authoring.jev`, including for hosts without a file-reading tool. No separate
|
|
83
132
|
TypeSafe skill install is required for DSL authoring. The
|
|
84
|
-
[source-read example](skills/author/examples/read-source-decision.json) preserves
|
|
133
|
+
[source-read example](../dsl/skills/author/examples/read-source-decision.json) preserves
|
|
85
134
|
original tool evidence through a Jev decision and deterministic projection; its
|
|
86
135
|
fictional tests demonstrate wiring, not measured semantic accuracy. Explicit
|
|
87
136
|
`tools: []` disables tools for an agent node; omission inherits the host allowlist.
|
|
@@ -164,7 +213,7 @@ const result = await runWorkflow(workflow, input, {
|
|
|
164
213
|
});
|
|
165
214
|
```
|
|
166
215
|
|
|
167
|
-
`ctx` supplies the active model and public model registry. Tools are explicitly supplied and receive the original context; outer hooks are not copied. The captured active model remains fixed for the run.
|
|
216
|
+
`ctx` supplies the active model and public model registry. Tools are explicitly supplied and receive the original context; outer hooks are not copied. The captured active model remains fixed for the run. A node thinks at its own `thinking` level; a node that names none thinks at medium, not at the host session's level, unless the runner's own `thinkingLevel` option sets another. Thinking is never off: a level of `off` runs at medium. The runner bounds turns, submissions, and time, and refuses an absent model instead of selecting a fallback. These limits do not impose token or spending caps.
|
|
168
217
|
|
|
169
218
|
For a standalone application, `createPiRunner(options)` accepts a configured SDK `ModelRuntime` and its selected `model`. Supply `tools` explicitly. It loads no ambient filesystem tools, workspace instructions, extensions, prompt templates, or skills. `createPiHostSessionFactory(ctx)` exposes the same native child session transport for integrations that own their own runner. Factories are unnecessary for ordinary extension use.
|
|
170
219
|
|
|
@@ -174,28 +223,51 @@ The child model delivers `{value: result}` through its `submit` tool. Schema and
|
|
|
174
223
|
|
|
175
224
|
## SDK: retain candidates and check fixed fixtures
|
|
176
225
|
|
|
177
|
-
|
|
226
|
+
`authorWorkflow` lives in `@parcha/agentrun-dsl` beside the language it teaches. It saves candidate files separately from the native extension's current draft and named procedure revisions. Give it a Pi runner with no host tools, and bound its submissions by the candidate limit:
|
|
178
227
|
|
|
179
228
|
```js
|
|
180
|
-
import { authorWorkflow } from '@parcha/agentrun-
|
|
229
|
+
import { authorWorkflow } from '@parcha/agentrun-dsl';
|
|
230
|
+
import { createPiRunner } from '@parcha/agentrun-pi';
|
|
181
231
|
|
|
182
232
|
const candidate = await authorWorkflow({
|
|
183
233
|
request: 'Extract a numeric count from text',
|
|
184
234
|
outputDir: './candidates',
|
|
185
235
|
inputKeys: ['text'],
|
|
186
|
-
|
|
236
|
+
runNode: createPiRunner({ ...options, tools: [], maxSubmissions: 4 }), // Your configured PiRunnerOptions.
|
|
187
237
|
maxCandidates: 4,
|
|
188
238
|
acceptance: workflow => checkAgainstYourFixtures(workflow),
|
|
189
239
|
});
|
|
190
240
|
console.log(candidate.path, candidate.checks);
|
|
191
241
|
```
|
|
192
242
|
|
|
193
|
-
The host owns `checkAgainstYourFixtures`: return diagnostics, or `[]` to accept. Without that callback, acceptance is labeled `structural` and does not establish correct behavior. The author retains each submitted version and its feedback, never activates it, and cannot edit the host's acceptance checks.
|
|
243
|
+
The host owns `checkAgainstYourFixtures`: return diagnostics, or `[]` to accept. Without that callback, acceptance is labeled `structural` and does not establish correct behavior. The author retains each submitted version and its feedback, never activates it, and cannot edit the host's acceptance checks. See [the author contract](../../docs/authoring.md#one-author-contract) for the host addendum.
|
|
194
244
|
|
|
195
245
|
With `rubricSections`, every supplied section must appear on every generated LLM node, including child workflows. The author conservatively rejects Jev nodes, semantic `ask` predicates, and `verify` clauses under that policy; those need a separately reviewed question contract. Schema properties named `node`, `verify`, or `predicate` remain ordinary data. Supply the authoritative rubric as `deps.sop` when executing the candidate.
|
|
196
246
|
|
|
197
247
|
The standalone CLI remains available as `node packages/pi/dist/cli.js --help` from source. It imports a trusted caller-owned `--config` module for `author` and `run`; importing that module executes its code. `validate` can evaluate code probes. Review executable workflows before using it.
|
|
198
248
|
|
|
249
|
+
## Sign in, pick a model, check keys
|
|
250
|
+
|
|
251
|
+
`@parcha/agentrun-pi/signin` is what a demo, or your own app, runs before its first model call. It is the only entry of this package that loads `@earendil-works/pi-ai`, an optional peer: install it beside the package (`npm install @earendil-works/pi-ai`) to use this entry. No other entry imports it.
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import { builtinModels } from '@earendil-works/pi-ai/providers/all';
|
|
255
|
+
import { checkKeys, resolveModel, usageLimitError } from '@parcha/agentrun-pi/signin';
|
|
256
|
+
|
|
257
|
+
const report = checkKeys(spec, { live: true, dir: demoDir });
|
|
258
|
+
if (!report.ok) { console.error(report.text); process.exit(report.exitCode); } // 2: nothing was sent
|
|
259
|
+
const { model, getApiKey, source } = await resolveModel(spec, { provider, model: modelId });
|
|
260
|
+
const message = await builtinModels().completeSimple(model, context, { apiKey: await getApiKey() });
|
|
261
|
+
if (usageLimitError(message)) throw usageLimitError(message);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- `checkKeys(spec, options)` lists what this run needs and what is missing: each key by its env name and where to create it, each program on `PATH` (refused outside its `platforms`, or below its `min` version as the first x.y.z that `<cmd> --version` prints), and each package as the demo's directory resolves it, with the exact `npm install --no-save <name>@<version>` line when it is missing. An item with `for: 'live'` or `for: 'kill'` counts only in that mode. A `oneOf` choice is met by any member, and an array member only by all of its items. A Jev judge asks for `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY`. The check sends no request and prompts for nothing; a key's value is never printed.
|
|
265
|
+
- `checkSetupPrompt(spec, text)` checks a demo's setup prompt against its spec: every key env name, program command and package name in the spec appears in it, and it names no credential env name (`*_API_KEY`, `*_KEY`, `*_TOKEN*`, `*_SECRET*`, `*_PROJECT_ID`) the spec does not list. `node scripts/check-setup-prompts.mjs` runs it for every `examples/*/demo.json` in CI.
|
|
266
|
+
- `resolveModel(spec, options)` picks the model and its key. An explicit `provider` (and `model`) with that provider's env key wins, passed on as an explicit key; Anthropic takes `ANTHROPIC_API_KEY` only. Otherwise a stored ChatGPT sign-in is used, and the model is chosen from the account's own `GET /v1/models` by what the spec needs (text, tools, images): the spec's default when the account lists it, else the newest one pi-ai knows. With neither, it runs the sign-in. A sign-in's model takes only streaming Responses calls with `store: false` (`responsesOnly`, `unsupported`). The spec's eyes model resolves the same way: an explicit `eyes` model with its key, else the lead model when it meets the eyes' needs, else a `ResolveError` naming the gap; the ChatGPT plan takes no video input. The result serializes without its key.
|
|
267
|
+
- `signIn(options)` runs pi-ai's own Sign in with ChatGPT once per install, with this app's name and a host id kept for the install. pi-ai's callback port is fixed at 1455; when another login holds it, the sign-in fails at once and says to finish or cancel that login. A pi-ai that ignores the app name (before 1.1) is refused before the page opens.
|
|
268
|
+
- The sign-in lives in `$AGENTRUN_HOME/auth.json` (default `~/.config/agentrun/`): the directory is 0700 and the file 0600, written whole and renamed into place. An existing directory others can read is refused, not changed. Refreshes are serialized across processes by an operating-system lock, an exclusive SQLite transaction on `auth.json.lock` (an empty database beside it, through `node:sqlite`). The system frees it when its holder exits or is killed, so a crashed run leaves no lock to clear, the rotating refresh token is never sent twice, and a process that waited reads the new token. On Node 22, `node:sqlite` prints one experimental warning when the first write loads it. A token never reaches a log, a URL or a recording.
|
|
269
|
+
- The plan's usage limit (`subscription_sharing_usage_limit_exceeded`) maps to `UsageLimitError`: "Your ChatGPT plan's usage limit is reached; wait, or set a provider key." It is never retried.
|
|
270
|
+
|
|
199
271
|
## Develop from source
|
|
200
272
|
|
|
201
273
|
To change the extension itself, clone and build the repository:
|
|
@@ -205,19 +277,24 @@ git clone --branch main --single-branch https://github.com/Parcha-ai/agentrun.gi
|
|
|
205
277
|
cd agentrun
|
|
206
278
|
npm ci --ignore-scripts
|
|
207
279
|
npm run build
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
agentrun_checkout="$PWD"
|
|
281
|
+
mkdir -p ../agentrun-pi-project
|
|
282
|
+
cd ../agentrun-pi-project
|
|
283
|
+
"$agentrun_checkout/node_modules/.bin/pi" install "$agentrun_checkout/packages/pi" -l
|
|
284
|
+
"$agentrun_checkout/node_modules/.bin/pi" --offline
|
|
210
285
|
```
|
|
211
286
|
|
|
212
|
-
|
|
287
|
+
These commands create a separate `agentrun-pi-project` beside the checkout. You can use your own project directory instead. Pi loads the extension and skill from the checkout, so keep it in place. After source changes, rebuild and run `/reload`.
|
|
213
288
|
|
|
214
289
|
## Dependencies and retained data
|
|
215
290
|
|
|
216
|
-
Pi is optional for `@parcha/agentrun-dsl`. This package
|
|
291
|
+
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 `@earendil-works/pi-ai` for the sign-in entry only) 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.
|
|
292
|
+
|
|
293
|
+
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`). This release supports Pi 1.0.4 to below 1.4, and CI tests 1.0.4, 1.1.0, 1.2.0 and 1.3.0; peer ranges at `*` do not make an older or newer host compatible. The sign-in entry needs pi-ai 1.1.0 or later: pi-ai 1.0.x ignores the app name and would put pi's name on ChatGPT's sign-in page, so `signIn` refuses it with `SignInError` (`APP_NAME_IGNORED`) before the page is shown. 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.
|
|
217
294
|
|
|
218
|
-
The
|
|
295
|
+
The durable entry (`@parcha/agentrun-pi/durable`) takes pi-durable and chord as optional peers from 1.0.4 to below 1.4; CI runs it on 1.0.4, 1.1.0, 1.2.0 and 1.3.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.
|
|
219
296
|
|
|
220
|
-
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
|
|
297
|
+
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.
|
|
221
298
|
|
|
222
299
|
## License
|
|
223
300
|
|
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { runWorkflow, validateWorkflow } from "@parcha/agentrun-dsl";
|
|
6
|
-
import {
|
|
7
|
-
import { createPiRunner } from "./runner.js";
|
|
5
|
+
import { authorWorkflow, runWorkflow, validateWorkflow } from "@parcha/agentrun-dsl";
|
|
6
|
+
import { runScriptedDemo } from "./demo-cli.js";
|
|
8
7
|
const help = `Usage:
|
|
8
|
+
agentrun-pi demo [empty] scripted demo; no key, no model calls, no network
|
|
9
9
|
agentrun-pi validate workflow.json [--inputs key1,key2]
|
|
10
10
|
agentrun-pi author --config ./pi.config.mjs --out ./candidates "Describe the workflow"
|
|
11
11
|
agentrun-pi run --config ./pi.config.mjs workflow.json input.json
|
|
12
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.
|
|
13
14
|
Config is trusted JavaScript exporting default PiRunnerOptions, optionally deps (Jev/effect adapters).
|
|
14
15
|
Author saves candidates; it does not execute or activate them.
|
|
15
16
|
Validate may execute code probes. Run executes trusted workflow code and exits 2 on escalation.
|
|
@@ -30,6 +31,12 @@ async function main(args) {
|
|
|
30
31
|
args.splice(index, 2);
|
|
31
32
|
return value;
|
|
32
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
|
+
}
|
|
33
40
|
const configPath = flag("--config");
|
|
34
41
|
const outputDir = flag("--out") ?? "./candidates";
|
|
35
42
|
const inputKeys = (flag("--inputs") ?? "").split(",").filter(Boolean);
|
|
@@ -47,11 +54,15 @@ async function main(args) {
|
|
|
47
54
|
throw new Error(help);
|
|
48
55
|
if (!configPath)
|
|
49
56
|
throw new Error("Supply --config with an explicit model and modelRuntime");
|
|
57
|
+
const { createPiRunner } = await import("./runner.js");
|
|
50
58
|
const config = await import(pathToFileURL(resolve(configPath)).href);
|
|
51
59
|
if (command === "author") {
|
|
52
60
|
if (args.length !== 1)
|
|
53
61
|
throw new Error(help);
|
|
54
|
-
const
|
|
62
|
+
const maxCandidates = 4;
|
|
63
|
+
// The author session gets no host tools and stops at the candidate limit.
|
|
64
|
+
const runNode = createPiRunner({ ...config.default, tools: [], maxSubmissions: maxCandidates });
|
|
65
|
+
const authored = await authorWorkflow({ request: args[0], outputDir, inputKeys, runNode, maxCandidates });
|
|
55
66
|
console.log(JSON.stringify({ path: authored.path, candidates: authored.candidates, checks: authored.checks }, null, 2));
|
|
56
67
|
}
|
|
57
68
|
else {
|
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
|
+
}
|
package/dist/demo.js
CHANGED
|
@@ -8,6 +8,7 @@ export function demoSearchTool() {
|
|
|
8
8
|
name: 'search', label: 'Search example sources',
|
|
9
9
|
description: 'Read the bundled fictional wiki-migration sources. This does not search the internet.',
|
|
10
10
|
parameters: { type: 'object', properties: { question: { type: 'string' } }, required: ['question'], additionalProperties: false },
|
|
11
|
+
resultSchema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'object' } } }, required: ['sources'], additionalProperties: false },
|
|
11
12
|
async execute(_id, _args, signal) {
|
|
12
13
|
signal?.throwIfAborted();
|
|
13
14
|
return { content: [{ type: 'text', text: JSON.stringify({ sources: demoSources }) }], details: { sources: structuredClone(demoSources) } };
|
|
@@ -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
|
+
}
|