@parcha/agentrun-kernel 0.0.0-stage → 0.1.0-beta.29

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +52 -2
  5. package/dist/delegate.d.ts +87 -0
  6. package/dist/delegate.js +143 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.js +37 -0
  9. package/dist/host/alive.d.ts +2 -0
  10. package/dist/host/alive.js +28 -0
  11. package/dist/host/box-slot.d.ts +26 -0
  12. package/dist/host/box-slot.js +112 -0
  13. package/dist/host/box.d.ts +104 -0
  14. package/dist/host/box.js +16 -0
  15. package/dist/host/egress-proxy.d.ts +45 -0
  16. package/dist/host/egress-proxy.js +416 -0
  17. package/dist/host/egress-table.d.ts +24 -0
  18. package/dist/host/egress-table.js +165 -0
  19. package/dist/host/index.d.ts +16 -0
  20. package/dist/host/index.js +12 -0
  21. package/dist/host/overhead.d.ts +53 -0
  22. package/dist/host/overhead.js +75 -0
  23. package/dist/host/parent-death.d.ts +15 -0
  24. package/dist/host/parent-death.js +42 -0
  25. package/dist/host/parent-watch.d.ts +1 -0
  26. package/dist/host/parent-watch.js +38 -0
  27. package/dist/host/pinned-binary.d.ts +6 -0
  28. package/dist/host/pinned-binary.js +48 -0
  29. package/dist/host/proxy.d.ts +179 -0
  30. package/dist/host/proxy.js +629 -0
  31. package/dist/host/serial.d.ts +3 -0
  32. package/dist/host/serial.js +10 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +6 -0
  35. package/dist/kernel/blobs.d.ts +16 -0
  36. package/dist/kernel/blobs.js +59 -0
  37. package/dist/kernel/drive.d.ts +35 -0
  38. package/dist/kernel/drive.js +202 -0
  39. package/dist/kernel/errors.d.ts +27 -0
  40. package/dist/kernel/errors.js +42 -0
  41. package/dist/kernel/follow-up.d.ts +67 -0
  42. package/dist/kernel/follow-up.js +146 -0
  43. package/dist/kernel/halt.d.ts +11 -0
  44. package/dist/kernel/halt.js +17 -0
  45. package/dist/kernel/index.d.ts +8 -0
  46. package/dist/kernel/index.js +5 -0
  47. package/dist/kernel/types.d.ts +120 -0
  48. package/dist/kernel/types.js +1 -0
  49. package/dist/process/builtins.d.ts +142 -0
  50. package/dist/process/builtins.js +207 -0
  51. package/dist/process/inbox.d.ts +29 -0
  52. package/dist/process/inbox.js +52 -0
  53. package/dist/process/index.d.ts +9 -0
  54. package/dist/process/index.js +11 -0
  55. package/dist/process/journal.d.ts +111 -0
  56. package/dist/process/journal.js +232 -0
  57. package/dist/process/records.d.ts +143 -0
  58. package/dist/process/records.js +142 -0
  59. package/dist/process/recovery.d.ts +76 -0
  60. package/dist/process/recovery.js +139 -0
  61. package/dist/process/settle.d.ts +11 -0
  62. package/dist/process/settle.js +20 -0
  63. package/dist/process/tail.d.ts +20 -0
  64. package/dist/process/tail.js +53 -0
  65. package/dist/process/tools.d.ts +105 -0
  66. package/dist/process/tools.js +120 -0
  67. package/dist/process/zod.d.ts +32 -0
  68. package/dist/process/zod.js +109 -0
  69. package/dist/runner.d.ts +154 -0
  70. package/dist/runner.js +547 -0
  71. package/dist/schema-check-cases.d.ts +7 -0
  72. package/dist/schema-check-cases.js +73 -0
  73. package/dist/schema-check.d.ts +3 -0
  74. package/dist/schema-check.js +88 -0
  75. package/dist/skill-files.d.ts +23 -0
  76. package/dist/skill-files.js +136 -0
  77. package/dist/testing.d.ts +91 -0
  78. package/dist/testing.js +196 -0
  79. package/dist/types.d.ts +237 -0
  80. package/dist/types.js +10 -0
  81. package/package.json +74 -4
  82. package/vendor/durable-harness/UPSTREAM.json +67 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,135 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## 0.1.0-beta.29 (2026-11-04)
6
+
7
+ - `@parcha/agentrun-adapters/kernel` and `@parcha/agentrun-adapters/process`: the durable kernel a harness adapter runs on, over the run's session store, and the shared layer for harnesses that run as a child process.
8
+ - An effect is its intent, then at most one outcome, each durable before what follows: a completed or failed effect is answered from its records and never runs again. When a drive opens, an intent a dead process left open is run again if its tool is `safe`, and recorded failed and interrupted if it is `never` ("it may or may not have taken effect"), never run again.
9
+ - A drive writes its session one append at a time, in call order (a submit's input, an effect's intent and outcome, a journal's blobs), so its state folds the records in the log's order whatever order a writer resolves them in. A failed append rejects for its caller only; the ones after it still run.
10
+ - `Drive.refresh()` re-folds the drive's own records and reads nothing: the drive is the session's only writer, and door input is already folded by `submit`. (A read that resolved after a newer append put older state back, so a completed effect could look unfinished and run again.)
11
+ - A built-in call begun twice (the same execution delivered twice) is never run twice: `BuiltinCalls.begin` reserves the call before it awaits anything, and a second `begin` waits for the first's outcome and answers with it from the log; when that outcome could not be written, the drive stops and the second `begin` rejects with the drive's stop reason, never with an answer the log does not hold.
12
+ - One drive runs a key once at a time: a second `effect()` call for a key already in flight in that drive, even one issued before the first's intent is durable, is refused with `Fatal` and its function does not run.
13
+ - Input is placed before it is sent, and a withdraw that lands first wins: a placement that names a withdrawn submission (from a `pending()` read before the withdraw) places nothing, and its effect throws `Restart` without running (before writing anything, when the withdraw is already in the drive's state), so the rebuilt drive sends only what is still pending.
14
+ - A large native record is kept as a blob in the session's own records, named by its content's sha256: `writeBlob` refuses a hash that is not its text's (`TypeError`), and `readBlobs` returns a blob only when one write's pieces are all there and join to text with that hash, so a write again under another piece size never mixes with the first.
15
+ - `./process` has the native journal, the transcript fold, the recovery note, the tool bridge, the hook-pair calls of a harness's built-ins and the inbox. These are copied from another project and listed, with each file's upstream blob and sha256, in `vendor/durable-harness/UPSTREAM.json`; `NOTICE` credits it.
16
+ - One process writes a session at a time, and the store enforces it. `SessionStore` is now `read(key)` and `open(key)`, which returns the session's writer and fences every writer opened earlier for that key, whether or not the new one has written anything; a fenced writer's `append` is refused with `SessionFenced` and writes nothing. The runner opens one writer per attempt before it reads the session and passes it to the adapter as `SessionIO.writer` (it was `store`); a runner whose writer is fenced ends `fenced`, not final, and delivers nothing. `openDrive` takes the writer (it took `store` and `key`), and a stale drive's refused write is a `Restart`. A host's store must implement `open` with this fence.
17
+ - New package `@parcha/agentrun-adapters`, held (not on npm yet): the seam for agent harnesses other than Pi. `createHarnessRunner(adapter, options)` is exactly the interpreter's `runNode`, so a DSL `agent` node runs on any harness that implements `AgentAdapter`, the way `createPiRunner` runs it on Pi.
18
+ - The node's record arrives through an injected `submit` tool (the node's schema under `value`); a review that does not accept it is answered in the same session.
19
+ - Every paid request passes the runner first: the turn cap (`maxTurns`, default 8, and a node's own `max_turns` within it), then the host's `beforeRequest`, which receives the node's `budget_usd`, what its session has spent, its label, key and execution path, and may refuse the request. `maxSubmissions` (default 5) and `timeoutMs` (default 120 s) are the Pi runner's.
20
+ - Under `withRecovery` a session is keyed by its step's `sessionId`: a delivered record is answered from the session's records with nothing run, and a session a process died inside is resumed with nothing paid twice. Without a step each call opens a fresh session.
21
+ - Turns sent to a node have their own count, against its turn limit (or `maxSends`, default 100, when the limit is null); running out of sends is `turn_limit`, and its message says the cap was on sends. Turns that end with no paid request and no accepted submission end the node `no_progress` after three in a row. Turns, spend and `max_turns` still count paid requests only.
22
+ - A turn limit, no progress, an unknown tool, a fork the harness cannot make and a host refusal fail the node with `final: true`.
23
+ - Every event names its node (session key, label, execution path).
24
+ - `./testing` has `memorySessionStore` and `fauxAdapter`, a scripted harness with no model and no network.
25
+ - The model proxy's `origin` may now carry a path: a gateway that serves the API under one (`https://gateway.example/api`) gets each request at that path plus the route's (`/api/v1/...`). Before, the path was dropped and the request went to the origin's own `/v1/...`. A base with userinfo, a query, a fragment or a backslash is refused when the proxy starts, so the provider's key goes only where the base reads.
26
+ - `boxSlot` (`@parcha/agentrun-kernel/host`) runs one session's harness boxes one at a time, whatever the isolation: how a box starts and how it is reaped are its arguments, in the shapes of the kernel's new `Box` (`host/box.ts`: `BoxSpec`, `BoxRecord`, `BoxRecorder`, `BoxedChild`, `Reaped`, `BoxError`).
27
+ - `start` is synchronous. It is refused with `BoxError` `box_busy` while the session's last box is live, being reaped, or was not counted empty. The box's start writes its record before the spawn (`ahead`) and before its first process is let go (`started`).
28
+ - The box is reaped as soon as its process exits or is never let go. Only the reap's own count frees the slot. The box's exit status and output are never read, since anything in the box can forge them. A box left with processes, or one nothing could count, rejects with `box_not_empty` and keeps the session from starting another. `adopt` reaps the record a session's last box left before anything starts.
29
+ - serve's `writeDurableSync` and `writeDurable` sync the new file and its directory before they return. `boxRecorder` writes a run's `box.json` with them, and refuses a record that names no writer, since such a record could never be cleared safely.
30
+ - A box that exits while its `started` record is still being written is reaped once that write lands, by the record it wrote. A box that has ended records nothing more, so a late write never replaces the next box's record.
31
+ - The durable writes write every byte or fail. A short write (a disk nearly full) is never synced and renamed as a whole record; the recorder refuses it with `box_record_failed` and leaves no part behind.
32
+ - A host's gateway headers reach the provider on every model request a Claude Agent session makes: `claudeAgentAdapter({ provider: { ..., headers: { "x-litellm-tags": "job:...,purpose:...,run:..." } } })`, the same LiteLLM spend-tag header a pi lead's model requests carry, so a gateway attributes the Claude lead's spend to its job. The session's model proxy (`startModelProxy({ headers })` in `@parcha/agentrun-kernel/host`) adds them to every forwarded request, paid and free, after the client's own headers (so the CLI can neither set nor override them) and before the credential's. They are checked when the adapter is made (`hostHeaderProblems`): a token name that is not the proxy's own (`authorization`, `x-api-key`, `host`, `content-*`, `anthropic-*`, ...), and a one-line value of at most 1024 characters. A host sends a gateway's headers only to that gateway.
33
+ - `claudeAgentAdapter({ webSearch: true })` turns on Claude's own WebSearch, and nothing else of the CLI's: the model may call it, the PreToolUse hook lets it through with its intent recorded before it runs (safe to repeat after a crash), and the system prompt's note names it. The CLI runs each search as a request of its own through the session's model proxy, which then admits Anthropic's web search server tool exactly as the pinned CLI sends it (`web_search_20250305`, at most 8 searches, the domains the model named); every other hosted tool, a later web search version, and every server tool's block stay refused. Each search the provider reports (`usage.server_tool_use.web_search_requests`, read from a stream's last `message_delta` or a whole body) is booked at $10 per 1,000 searches (`CLAUDE_WEB_SEARCH_USD`) on top of the host's `price`, into the session's cost and so its spend cap. A request with the web search tool reports no input bound (`inputTokensMax` undefined): the provider bills the pages it reads as input. In `@parcha/agentrun-kernel`, `ProviderPolicy.serverInputOverhead` may now return `undefined`, a policy's word that a body has no input bound: the model proxy leaves `inputTokensMax` undefined (never 0) and, unlike a throw or a non-count, reports no failure. WebFetch stays off. Default off: a session without the option runs as before.
34
+ - **The Codex adapter takes the host's skills, and its sessions no longer see Codex's bundled ones**
35
+ - `codexAdapter({ skills: [{ name, files }] })`: Codex lists each skill to the model by its name and description, and the model reads its `SKILL.md` and its other files with `read_skill_file`, the shared read-only host tool (Codex has no shell here). The skills are written into each session's `CODEX_HOME` at the start of every drive, where Codex lists a user's skills in a turn with its built-ins off; it lists none from a working directory's `.agents/skills` there. A host tool named `read_skill_file` is refused when the adapter has skills.
36
+ - Codex's bundled skills (imagegen, openai-docs, skill-creator, skill-installer) are off in every Codex session, with skills or without (`[skills.bundled] enabled = false`). Before, Codex installed them at every start and listed them to the model, so a session was shown skills its host never gave it.
37
+ - The skills are checked when the adapter is made, and a bad one throws before the binary is looked for. The check, `skillProblems`, and the skill's shape, `HostSkill`, are now in `@parcha/agentrun-kernel` beside `readSkillFileTool`. The Claude adapter's `skillProblems` and `ClaudeAgentSkill` are that check and shape, re-exported, so nothing changes for its users; the Codex adapter exports the same `skillProblems`.
38
+ - The local Codex machine takes a seed of files for one of its private directories (`seed: { dir, files }`, each file's text by its relative path), written at acquire and again after every wipe. A path that is absolute, empty, climbs or starts with a dot, or a directory not the machine's own, is refused when the machine is made.
39
+ - The egress proxy can take a host list: `startEgressProxy({ ..., allow: ["pypi.org", "*.pythonhosted.org"] })` lets a box reach only those hosts, on ports 80 and 443.
40
+ - An entry is an exact name or `*.suffix`, which covers the names below the suffix but not the suffix itself. Matching ignores case and a trailing dot.
41
+ - A host off the list, any IP literal and any other port are refused before any lookup, so a name off the list never leaves the host. The refusal is `403` (SOCKS5 reply 2) with the reason in plain words, for example `example.com is not on this box's network list`, followed by the caller's `allowHint`. Its `x-egress-refused` and log class is `not-listed`.
42
+ - A listed name then meets the address rule as before: one that resolves to private or metadata space is still refused.
43
+ - With no `allow`, nothing changes. `parseAllow` is exported, so a caller checks its setting with the code the proxy enforces it with.
44
+ - A box's egress proxy and its address rule (`@parcha/agentrun-kernel/host`): `startEgressProxy` serves HTTP CONNECT, plain HTTP forwarding and SOCKS5 CONNECT on a 0600 Unix socket, to the public internet only, and `checkAddress` is the one rule it applies. The rule judges the binary address a socket would connect to, so the text a client wrote never matters, and it refuses loopback, private, shared and link-local space, cloud metadata addresses, multicast, broadcast, reserved, benchmarking and documentation blocks, IPv6 outside global unicast, and the host's own interface addresses. An IPv6 address that carries an IPv4 address is judged by the IPv4 inside.
45
+ - A name is resolved once, every address in the answer is checked, and the connection goes to exactly the checked address.
46
+ - A refusal is answered before any socket opens: 403 with the class as its reason, or SOCKS reply 2. SOCKS5 BIND and UDP ASSOCIATE, an HTTP upgrade, and a Host header that names another host than the request URI are refused by name.
47
+ - A redirect is never followed: the client's next request is checked like any other.
48
+ - Each run has a cap on connections open at once and a budget of connections in all, each refused by name. The log carries the host, the port, the address and the class, never a path, a query, a header or a body.
49
+ - `followUpOnce` in `./kernel`: one more turn in a session that has answered, sent at most once. Its intent is durable before the prompt leaves, so a process that dies inside it leaves it interrupted and no later process sends the prompt again; a follow-up turn a crash cut off is settled, never resumed, so it makes no paid request after the crash. The prompt is sent under a submission id of the follow-up's own, and the settle is named only that submission, so a person's message is never withdrawn. Its paid requests are held to a reserve of their own (`reserveUsd`, counted from its first request), and a request is refused while the session's spend is unknown. `followUpState` reads its state from its records, for a host deciding whether an open turn is the follow-up's.
50
+ - The harness packages are published from this release under their final names: `@parcha/agentrun-adapters` is `@parcha/agentrun-kernel`, `@parcha/agentrun-claude-agent` is `@parcha/agentrun-harness-claude`, `@parcha/agentrun-codex` is `@parcha/agentrun-harness-codex`. Their subpaths, directories, harness ids and `AGENTRUN_*` environment variables do not change.
51
+ - Messages these packages raise start with `agentrun-kernel:`, `agentrun-harness-claude:`, `agentrun-harness-codex:`.
52
+ - `delegateTool`: a lead hands one task to a named sub-agent, `delegate({ agent, task, context? })`, as an ordinary host tool for a lead on any harness. Each call runs a durable child `agent` node with the agent's own system prompt, only its own subset of the lead's tools, and a fixed answer shape (`DELEGATE_RESULT_SCHEMA`: `answer`, `citations`, `gaps`).
53
+ The child keeps its session under `<lead's key>/delegate:<call id>`, so the call is replay-safe: a finished child answers from its record, and one a crash cut off resumes from its own store without sending its paid requests again. Its requests go through the lead's `beforeRequest`, and its deadline is the lead's or the agent's share of the time left. Children run one at a time, never delegate further, and their events carry a `delegate` tag. `delegateProblems` checks a definition before the tool is made.
54
+ - A host tool's call context carries the runner's `sessionDir`, when it has one, so a tool that runs a node of its own (the delegate) gives its child a private directory the same way, by the child's key.
55
+ - `@parcha/agentrun-kernel/host`: a session's model proxy, provider-neutral. It listens on the loopback interface and admits only the session's own key (compared in constant time), only the routes a `ProviderPolicy` lists (by exact path text: no absolute URL, `..`, `//` or `%` escape, and no query but one the route lists exactly, forwarded as listed), and one strict JSON value with no duplicate key. A route may instead be answered by the proxy (a client's connectivity check) or be free (a token count): forwarded without asking the runner, and reporting no usage. A policy may rewrite or drop a listed header's value (a beta it does not know). The policy shapes or refuses the body; the runner's `beforeRequest` is asked before each paid request, and a refusal is answered with a status the client does not retry; the provider's credential is added by the proxy and never reaches the harness; each completed response's usage goes to `afterRequest`, and one cut off before its usage reports nothing. The proxy keeps no ledger: the runner alone accounts model spend.
56
+ - `./process` gains `waitForSubmissions` (a harness that keeps running while it waits for input) and `zodShapeFromJsonSchema` (a host tool's JSON Schema as a zod shape, with the zod the harness SDK uses), both copied with their provenance like the rest of the layer.
57
+ - A policy may say which model a checked body asks for (`ProviderPolicy.modelOf`): a session's smaller model, for a client's background calls, is then the model the runner is asked about and the one the request is priced at. Without it, or when it throws (logged), the request is booked as the session's model.
58
+ - A request that cannot be sent (no provider credential, one that could not be read, or credential headers the policy could not build) is refused before the runner is asked: it is never admitted or booked, and spends no turn. (It was admitted, then booked at nothing.)
59
+ - `close()` admits and sends nothing more: a request that was waiting on the runner when the proxy closed is refused and released, booked once at nothing.
60
+ - A body's `__proto__` key is parsed as an own field; the parsed object's prototype never changes.
61
+ - A client that reads slowly holds the provider back: the proxy reads no more of an answer until what it wrote has drained, and each wait leaves no listener behind on the response.
62
+ - A client that leaves once it has read a successful answer's usage, while the provider still holds the stream open (Codex does, at its completion event), is booked at that usage; one that leaves before any usage is in what was read stays an unknown cost.
63
+ - `modelPortFor(writer, options)`: the runner's booking of paid model requests, on its own, for a host that drives an adapter session directly (a chat run, not a DSL node). It writes the same records the runner does, `runner.request` before a request leaves and `runner.turn` with its cost when it answers, applies the same turn limit and the host's `beforeRequest`, and reports `request_start` and `request_end`. `createHarnessRunner` is built on it.
64
+ - A paid request a crash cut off (its `runner.request` with no `runner.turn`) is booked once when the session is next opened (`bookCut()`): a `runner.turn` marked `cut`, at an unknown price, and a `request_end` whose usage says `cut: true`, `usd: null`, `estimated: true`, so a host's ledger shows the possible spend. It is booked once however many times the session is recovered.
65
+ - Paid requests may be in flight together, so each is booked under the turn it was admitted as: `ModelPort.beforeRequest` returns a ticket (`{ turn }`), the model proxy hands it back to that request's `afterRequest`, every `runner.turn` names its turn, and `request_end` reports it. A cut request is the admitted turn with no answer, not a count.
66
+ - Paid requests asked for at once are admitted one at a time (the turn limit, the host's `beforeRequest` and the durable `runner.request`), so two can no longer both pass a limit only one fits; once admitted they still run side by side. `beforeRequest` is told the session's requests in flight (`inFlight`, each `{ turn, model }`), so a host's budget can hold each one's worst case until it answers: `spentUsd` counts answered requests only.
67
+ - Each booked request carries the ids its provider's response gave it (`providerIds`, by header name: the provider's `request-id`, a gateway's call id such as LiteLLM's `x-litellm-call-id`), on `runner.turn` and `request_end`, so a run's books can be joined to the provider's or gateway's log request by request. The proxy reads them when the response's headers arrive (`providerIdsOf`, exported from `./host`), before its body, and the port writes them then (`runner.response`, through the new optional `ModelPort.responded`): a request a crash cuts off after that point is booked as cut with its ids, since the provider saw it, and one cut before has none. Only id-named headers with id-shaped values are kept.
68
+ - `processAlive(pid)` from `@parcha/agentrun-kernel/host` says whether a process is running. A zombie, which has exited but whose parent has not reaped it, is not running, even though `kill(pid, 0)` still finds it. That happens to every orphan under a PID 1 that does not reap, such as a container started without an init. On Linux, the answer comes from the state in `/proc/<pid>/stat`; elsewhere, from `kill(pid, 0)`.
69
+ - A refusal the model proxy reports names who called: its log entry carries the client's `user-agent` as `userAgent` (at most 120 characters), and none when the client sent none. Another local process can find a session proxy's loopback port and call it without the session's key. It is still refused, and the log now shows it for what it is.
70
+ - A paid request whose response reports its cost is booked at that figure: `reportedCostOf` reads a gateway's cost header (LiteLLM's `x-litellm-response-cost`, or its `-original` when the discount and margin it reports are both 0) as the response's headers arrive, and the host's price table's figure is kept beside it as `tableUsd`. Only a positive number counts. A missing, malformed or zero figure (a streamed answer's headers leave before it is priced, and say `0.0`) leaves the table to decide, and that booking is now marked `estimated`. A run's `runner.turn` records keep `estimated` and `tableUsd`.
71
+ - A host that reads `estimated` now sees it on table-priced requests too, not only on an unknown cost.
72
+ - A host tool may answer with images (a screenshot): `HostTool.call`'s result takes `images: [{ mimeType, data }]` (base64) beside its text. The kernel records them with the call's outcome, so a call answered from its record answers with the same images.
73
+ - The Claude Agent adapter sends them to the model inline in the tool's result. It checks each image first, a type Anthropic's API takes (`image/jpeg`, `image/png`, `image/gif`, `image/webp`), base64, at most `maxImageBytes` (default 5 MB, `CLAUDE_AGENT_MAX_IMAGE_BYTES`; at most 10 MB), and makes an image it can't send the call's error, by the tool's name and the image (`toolImageProblems`).
74
+ - The session's model proxy lets an image through only inline inside a tool's result, and leaves out the note the CLI adds beside it naming the file it kept in its own session directory.
75
+ - A session resumed in a new directory still sends the image, from the store.
76
+ - `toolSchemaProblems(tool, schema)` lists what a harness adapter refuses in a host tool's input schema before a session starts, the same on every harness, each as `<tool>: <JSON pointer>: <why>`: a root that is not `{ "type": "object" }`, a schema that is not JSON or not valid JSON Schema 2020-12 (a `$schema` must name 2020-12, 2019-09, draft-07, -06 or -04), and a remote `$ref` (a local one into `$defs` is followed). An adapter's `check()` calls it first and then adds its own harness's measured refusals.
77
+ - The Claude Agent adapter's `check()` uses it, and also refuses a root with `anyOf`, `allOf` or `oneOf`, which the pinned CLI drops without a word, the tool missing from the session. Put the alternatives under a property. `open` refuses a session whose tools have any of these, naming each.
78
+ - A call's `tool_end` reaches the host only once its outcome is in the session's records, with the text recorded as `result`, and `createHarnessRunner(...).stop({ drainMs })` pauses every node a runner is running: no new turn, tool call or paid request starts, and the calls in flight run on until their outcomes are recorded, for at most `drainMs`, before the node fails `aborted`. Run again with the same step, the node resumes from its records, and a call that finished during the pause is never run or paid for again.
79
+ - Before, the Claude Agent adapter sent `tool_end` as the tool returned, before its outcome was written, and an interrupt cut off a call in flight, so a `safe` call ran again when its session resumed.
80
+ - `AdapterSession.interrupt` takes `{ drainMs }`. An adapter that cannot drain stops at once, as before. The Claude Agent adapter drains, and refuses a new turn while it does.
81
+ - A failed call's `tool_end` carries its recorded text too.
82
+ - A spend cap below one request's worst case at its model's output ceiling no longer refuses the run before it starts. Each request is limited to the output tokens what is left of the cap can buy, thinking included, and held at that, so the cap stays exact. A request is refused only when what is left cannot buy its least: 1,024 tokens for its answer, plus its thinking's least when it thinks. A $0.50 cap on Claude Sonnet now runs; before, any cap under about $1.30 could never send a request.
83
+ - A refusal says, in plain words, what the run spent of its cap, what its next request needs and the cap that would run it ("Not sent: ... Raise the cap to at least $0.48 to run it."), on the run's error and its timeline row, after the code `spend_cap_reached (out of budget): `, which pi-ai's retry takes as final. A request the cap refused counts as no model request in the run's usage, on every harness; a run whose only request was refused says "no model request sent". A reply the limit cut short says so on the run's timeline.
84
+ - The model proxy asks the host with a request's own output limit and its least (`outputTokensAsked`, `outputTokensLeast`), and writes the admitted limit (the ticket's `outputTokensMax`) into its body through a policy's new `limitOutput`; it never sends a request unlimited that was admitted with a limit, nor a body longer than the one its input was measured on. The runner's `beforeRequest` may answer `outputTokensMax` beside `holdUsd`, and holds each request in flight at the limit it was admitted with; a limit that is no whole number of tokens above 0 refuses the request, never sends it unlimited.
85
+ - A run that learns gets the skills its agent wrote beside its revision's own. Each is a folder `skills/<name>/` on the learning folder's `main`, copied into the run's `work/.agents/skills/<name>/`: pi and codex read its `SKILL.md` in their prompt, and claude-agent loads it through its Skill tool when its adapter takes it.
86
+ - A skill named like one of the revision's is the revision's. Only an active skill is served: one whose `SKILL.md` sets `status: pending` (held for its owner's OK), or any status but `active`, is not.
87
+ - Learned skills get their own budget, 256 KiB of files in all, and never more than the revision's skills leave of the 1 MiB an adapter takes of a run's skills, so they never push the revision's back into the prompt. The one changed last comes first, each whole while it fits; one that does not fit, or that holds one of the server's secrets, is left out.
88
+ - The run's pin (`runs/<id>/learning.json`) names the files laid (`skills`) and the skills left out (`skills_left_out`). A follow-up gets them laid again, fresh. A learned skill the run's work no longer holds whole when its process starts is left out of what the harness gets, and the run goes on.
89
+ - A symbolic link or a submodule at `skills/` or under it is a learning fault: the run starts with nothing learned (`seed_failed`).
90
+ - `@parcha/agentrun-kernel` exports `SKILL_BYTES`, the 1 MiB its `skillProblems` takes of a host's skills in all.
91
+ - A person retires a learned skill or a workflow draft as they retire a note: `POST /v2/agents/{id}/learning/files/{file}/retire` with If-Match on its `skills/<name>/SKILL.md` or its draft's `workflows/<name>/README.md` (percent-encoded). It sets `status: retired` in one host commit (`learning: retired by a person: <file>`), the learning view lists it `retired`, and no later run is given it. A stale ETag is 412, and the token may retire as it may retire a note; only an approval takes the owner's session.
92
+ - Claude and Codex runs keep their token counts and their model in the run's journal, as pi runs do. A `runner.turn` record now keeps its request's tokens (`usage: {input, output, cacheRead, cacheWrite}`), so a request re-booked when the run opens again keeps them; a cost written late (`agentrun.request_cost_late`) and a compaction's cost (`agentrun.request_cost`) carry the request's tokens in `data.usage`; and an adapter answer's `pi.assistant` entry names the model its request asked for, its provider still the harness. Tokens that are not known are left out, never written as counts. Journals written before this change open and re-book as before, with no tokens. No cost changes.
93
+ - A capped `claude-agent` or `codex` run goes on after one of its requests was cut, by a stop or by a crash, while its cap leaves room: the cut request counts at the worst case the cap held for it, not as a spend no one knows. Before, the model proxy booked a response cut short at an unknown cost and a crash left a request with no booking, so the run's spend became unknown and every later request was refused with `spend_cap_reached`.
94
+ - A harness runner's `beforeRequest` may answer `{ holdUsd }`, what the host holds for the request. The runner writes it on the request's `runner.request` record (`heldUsd`) before the request leaves, and the spend it shows the host (`spentUsd`) counts a request at that figure when the request's own cost never comes in: a response cut short, or a request a crash cut off, read by a later process. A request with no such figure (a session written before this release, or a host that holds none) leaves the spend unknown, as before. The cost booked for the request is still unknown, so the run's shown cost is unchanged.
95
+ - serve's cap holds each request it admits at its worst case (its measured input bound and its model's output ceiling, at the model's price) and hands that figure to the runner. A capped run whose process died mid-request is opened again and goes on at that figure, and is refused with the cap's message once the figure leaves no room.
96
+ - A turn that a halt ended with a reason now ends `failed`, naming it, never `interrupted` with the reason dropped: a harness's spend cap, its transcript mirror, a refused spawn. Only the session's own stop (its interrupt or its close, told apart by identity with the kernel's `SessionHalt`) and its host's stop end a turn `interrupted`. `abortedOutcome` decides it, and both adapters use it. This changes how spend-cap and mirror halts end a turn: a host that read `interrupted` for them now reads `failed` with the reason.
97
+ - A stop that comes while a paid request's record is being written now keeps the request from leaving. The runner checked for a stop only before that write. It now checks again once the record is in. A request stopped there is booked as withdrawn, a `runner.turn` with `usd: 0` and `unsent: true`, so a later process never books it as cut at an unknown price, and the session's spend stays known.
98
+ - Checking a harness's binary against its pins no longer reads the whole binary into memory, and runs once per binary in a process. `verifyCodexBinary` (the codex binary, 294 MiB) and `verifyClaudeAgentCli` (claude-agent's CLI, 231 MiB) each read the file whole to hash it, and agentrun-serve checked each again for every model it resolved: twice per harness in its start's probe, then once per save and once per run check, and a run's own process once more when it made its adapter. One start probe peaked at 395 MiB of memory, 675 MiB with a default model, past a 512 MiB container's limit. The hash now reads 1 MiB at a time, and a check that passed is kept for the process while the binary stays the same file (its path, device, inode, size, and modification and change times, with the pins); touched or replaced, it is checked again, and a failed check is never kept. The kernel's `@parcha/agentrun-kernel/host` exports both parts, `sha256OfFile` and `checkedOnce`.
99
+ - A claude-agent run whose tool call ended before its answer was paid (Claude Code's cost comes in after its response) stored the call's result before the call. Its timeline showed the result first, labelled without its argument, then an "interrupted" result for the same call when the run ended, and the run's tool metrics counted that call twice, once failed. The timeline and the metrics now match such a result to the call that comes after it: it shows after the call, with its argument, once; the call never reads as interrupted; and it counts once, under its call's name, by its call's tool's rule. Runs already stored that way read right too.
100
+ - `@parcha/agentrun-kernel/testing`: the faux harness's `calls` action takes `bookAfter: true`, which runs the calls before the answer's cost is booked, as with Claude Code.
101
+ - A harness's own process dies with the process that started it. The Claude Agent CLI runs detached, in its own process group, and does not exit when its stdin closes, so a host that was SIGKILLed (and ran no close) left its CLI running, reparented, one more per crash. `@parcha/agentrun-kernel/host` now has `parentWatched(child)`: on Linux with util-linux setpriv the child starts under `setpriv --pdeathsig KILL --` (the kernel kills it the moment its parent dies), elsewhere under a small node shim that kills it when it is reparented. The Claude Agent machine launches every child with it, inside the host's confinement (`launchCommand`: the confine prefix, then the watch, then the pinned binary), so the death signal is set after any switch of user the prefix makes. The watch adds nothing to the child's environment.
102
+ - A claude-agent or codex run works in the run's workspace, the directory its file tools write to. Before, the CLI's working directory was an empty one beside it (`runs/<id>/harness/work`): Claude told its model that path, so a file written at its absolute path was refused as "outside the workspace", and the answer quoted the server's data directory. In QA this happened in 3 of 4 Claude runs that wrote a file.
103
+ - `SessionIO.cwd`: a host may name the session's working directory, its workspace. The Claude and Codex adapters run their CLI there, and keep their own state in the session's private directory as before. git stops at the workspace's parent. serve passes the run's `work/`.
104
+ - A file tool's refusal, an error such as ENOENT, and a write's or edit's result on claude-agent and codex name paths in the workspace relative to it (`notes/a.md`, `../escape.md: outside the workspace`), never by the server's data directory. A read's result is the file's own text and is not changed.
105
+ - A host tool's result reaches the model whole on claude-agent and codex. The Claude Agent CLI saved an MCP tool's result past 50,000 characters (a long `web_fetch` page, a `monid` answer, a 50 KB `read`) to a file under its own config directory and told the model to read it, which the run's confinement refuses, so the model could not read the page; Codex kept only the two ends of a result past its truncation policy (10,000 tokens for the models its catalog holds, about 12,000 characters for another). `@parcha/agentrun-kernel` states the bound once, `HOST_RESULT_MAX_CHARS` (120,000 characters, over every host tool's largest result); the Claude Agent adapter lists each host tool with it as `anthropic/maxResultSizeChars` and sets `MAX_MCP_OUTPUT_TOKENS` to it, and the Codex adapter sets `tool_output_token_limit` to it as Codex counts it (four bytes of UTF-8 a token: 90,000).
106
+ `web_fetch` (and `fetch_page`) reads a page a part at a time: `offset` and `max_chars` (20,000 by default, at least 200, at most 50,000; a smaller one or an offset below 0 is refused before anything is fetched, saying so; a workflow's call node reads 50,000 by default) name the part, the Page gives `chars`, its length, and a part that is not the whole page has `truncated` while more follows, `next_offset` where it begins and `original_chars` where the whole length is known; an agent reads a line after the JSON saying which characters these are and the offset that reads on. A page whose HTML pi-browser cut at 1,000,000 characters before reading it gives `html_chars`, its whole length, on every part and no `original_chars`, and the line after its last part says `The page was cut at 1,000,000 characters of HTML before reading; the rest wasn't read.`; `converted: "text"` says its text is plain text, its conversion to markdown having run out of time. The text was cut at 60,000 characters with no way to read the rest. A page's links come with the part at offset 0, at most 200 in 20,000 characters, and a URL longer than 4,096 characters is refused before anything is sent.
107
+ `web_search` leaves out its least relevant results when its answer would pass 100,000 characters, and `omitted` counts them.
108
+ - A host tool's result reaches a harness at most `HOST_RESULT_MAX_CHARS` (120,000) characters long. The kernel's tool bridge, which every host tool's result passes on its way to Claude Agent or Codex, keeps a longer one's start, cut where a character ends, ends it with a line saying where it was cut (`[cut at N of M characters]`), and flags the call's `tool_end` `truncated: true`; the call's record keeps the whole result. On a Codex resume, a call the rollout left unanswered is answered from that record with the same cut. A skill file read with `read_skill_file`, a delegate's answer and a rejection listing every schema error reached the harness whole, however long (a 1 MiB skill file, a 6 MB answer).
109
+ - A node's review that rejects a candidate without a message now rejects it on a harness other than pi. `createHarnessRunner`'s `submit` read the review's `message` as the rejection, so a review answering `{ accepted: false }` with none let the candidate through as the node's result and told the model it was accepted; a null, empty or blank message rejected with no words. Each now rejects, and the model reads that the review rejected it without saying why, as on Pi's runner.
110
+ - A process that exited and was never reaped (a zombie, as an orphan becomes under a PID 1 that does not reap, such as a container started without an init) is no longer counted as running. `processAlive(pid)` (from `@parcha/agentrun-kernel/host`) reads the process's state on Linux and falls back to `kill(pid, 0)` elsewhere; serve uses it to watch a run's process, so a server that restarts after its run's process died relaunches the run instead of waiting on the zombie forever.
111
+ - A paid request's response ids are kept when their own write is refused. The ids a response's headers give (a gateway's call id, the provider's request id) are written as the headers arrive, before the client gets anything. When that write was refused, for example by a commit that timed out, they were dropped. The model proxy still goes on with the answer and says so once, and the booking now carries the ids, marked `late`, into its own next write: the next request's record, or this request's answer. A request cut off later by the process dying still keeps them. An answer whose record already holds the ids drops the carried copy, so they are never written twice. A process that dies inside that write, or before the booking writes again, still loses them; the request is then booked once, cut, at an unknown cost, with no ids.
112
+ - The model proxy now tells the host, before a paid request is sent, the most input tokens it can be billed for. A spend cap could only count a request's cost once it was billed, so a single request whose input alone was over what was left of the cap was still sent and paid. `beforeRequest` now carries `inputTokensMax`: the UTF-8 bytes of the body exactly as it is sent, plus the input the provider adds that the body does not carry. A byte-level BPE token covers at least one byte, so this never undercounts the body, however dense its text. Each `inFlight` entry carries its own `inputTokensMax` until that request answers, so a cap can hold every open request at its own bound. When the provider's addition cannot be read, `inputTokensMax` is undefined, never 0, and a cap treats that input as unknown.
113
+ - What a provider adds is in one place, `providerInputOverhead(api, shape)` from `@parcha/agentrun-kernel/host`. `api` is pi's API kind, and `shape` is whether the request has tools, its tool_choice and its image count. Each figure is the largest the provider documents for any current model:
114
+ - `anthropic-messages`: the tool-use system prompt the Messages API adds to a body with tools, 675 tokens for tool_choice auto or none and 804 for any or tool, plus 4,784 tokens for each image, its high-resolution tier's maximum.
115
+ - `openai-responses`, and the Azure and Codex Responses kinds: no prompt of their own, and 73,800 tokens for each image. That is 30,000 patches, the most an image may have, times 2.46, the largest model multiplier.
116
+ - `openai-completions`, which OpenAI-compatible gateways speak too: nothing for its per-message formatting, since the body's bytes already bound it; 804 tokens when the request has tools, whatever its tool_choice, the largest Anthropic tool-use prompt, since a gateway may route to an Anthropic model; and 73,800 tokens for each image.
117
+ - Every other API kind, among them pi's `mistral-conversations`, `bedrock-converse-stream`, `google-generative-ai`, `google-vertex` and `pi-messages`, and a faux `faux:<id>`: nothing for text, an assumed 2,048 tokens when the request has tools, and 73,800 tokens for each image. The tool allowance for these kinds is assumed, not documented; it is documented only for `anthropic-messages`, the OpenAI Responses kinds and `openai-completions`. `providerInputOverhead` never returns undefined: only a body that cannot be measured is unmeasured.
118
+ - Images are billed by their area, not their bytes, so a small compressed image of large dimensions is counted at its maximum. A Codex body with a file input (a PDF, billed by its pages) is left unmeasured.
119
+ - A provider policy says what its provider adds with `serverInputOverhead(body)`. The Claude and Codex policies read their body's shape and use the figures above.
120
+ - The faux adapter in `@parcha/agentrun-kernel/testing` measures its prompt's UTF-8 bytes for each request, as a real adapter's proxy measures its body.
121
+ - `inputTokensMaxOf(policy, body, sent)` is exported from `@parcha/agentrun-kernel/host`.
122
+ - The proxy serializes the body once before admission and sends exactly what it measured. A body that cannot be serialized is refused before the runner is asked, so it is never booked.
123
+ - A capped run that the person stops can go on. A request you stop now counts what it streamed, plus up to 1,024 output tokens the provider may have produced after the last word we read, and never more than it was held. Before, it counted at its whole hold. Since each request is limited to what is left of the cap, that hold was the rest of the cap. So a $1 run stopped after $0.004 of spend read $1 spent, and its next message failed with `spend_cap_reached`.
124
+ - **On pi:** a stopped request counts its input bound, each byte of text, thinking and tool calls it streamed as a token, and `STOP_ALLOWANCE_TOKENS` (1,024) of output, at the model's price, at most its hold. Its end mark (`agentrun.request_ended`) carries that figure as `heldUsd`, so a later process counts it the same.
125
+ - **On `claude-agent` and `codex`:**
126
+ - The model proxy reports a response its client left part way, its cost unknown, with the bytes it had sent (`stoppedAfterBytes`).
127
+ - A runner `beforeRequest` may answer `stoppedUsd(streamedTokens)` beside `holdUsd`. The runner counts such a response at the lesser of the two and writes the figure on its `runner.turn` (`heldUsd`, `stopped: true`).
128
+ - A response the provider ended early, or one cut by a crash, still counts at its hold.
129
+ - **Unchanged:** a request that ended on an error after its response began, a failed summary and a crash cut still count at the hold. The run's shown cost is unchanged: a stopped request reads unknown, beside its cost so far.
130
+ - A Claude or Codex turn the person stopped stays stopped when the run's process is replaced. Before, the next process found the stopped turn still open in the session and resumed it: it ran its tools and paid for its model requests again, and its answer came before the person's follow-up. Now serve's open reads the run's stop record and settles that turn, ended `interrupted`, with nothing sent, and the follow-up is a turn of its own. A turn a crash or a drain cut is resumed as before.
131
+ - A stop that lands before the harness took the person's message withdraws that message too, so it is never sent later: not with the follow-up in the same process, and not by a process that replaces it.
132
+ - `AdapterSession` has `settleOpenTurn()`: it records the open turn as ended, interrupted, withdraws the input the turn never placed, and sends nothing. The Claude Agent, Codex and faux adapters implement it. A harness that runs as a child process does both with `settleTurn(drive)` from `@parcha/agentrun-kernel/process`. An adapter written outside this repository without it keeps the old behaviour: its stopped turn is resumed.
133
+ - After `interrupt()`, the Claude Agent and Codex sessions read `open_turn` while the stopped turn is still open in their records. serve's stop settles it there; a runner's pause leaves it for the next process to resume.
134
+ - When the earlier process died before it wrote the stopped turn's end, the next one writes it, `interrupted`, before the follow-up's turn starts.
135
+ - The adapters' tests that need one thing to happen before another wait for it, never for a fixed time. "close() while a request waits for the runner" failed on a loaded runner when reading the credential took longer than the 30 ms it slept: close() came first and the request was refused before it was admitted. It now closes once the runner's hook is entered, with a credential that takes 60 ms, so a timer in its place fails it. The same kind of wait is replaced in the proxy, runner, runner-stop and kernel tests that asserted what had to happen first.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ AgentRun
2
+ Copyright 2026 Parcha Labs, Inc.
3
+
4
+ This product includes software from vercel-labs/durable-harness (https://github.com/vercel-labs/durable-harness), Copyright 2026 Vercel Inc., licensed under the Apache License 2.0.
5
+
6
+ The copied files are listed in vendor/durable-harness/UPSTREAM.json. Two kernel files written for AgentRun, src/kernel/types.ts and src/kernel/errors.ts, keep one declaration each identical to that project's harness-world package (EffectRecord's fields and the EffectFailure class), so the copied files compile against them; UPSTREAM.json records them as "interfaces matched".
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # @parcha/agentrun-kernel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Agent harnesses other than Pi (Claude Agent and Codex) behind one seam, so a DSL `agent` node and an AgentRun run can run on a
4
+ harness adapter the way they run on Pi, durable in AgentRun's own store.
5
+
6
+ - `createHarnessRunner(adapter, options)` is exactly the interpreter's `runNode`. A node's record arrives through an
7
+ injected `submit` tool whose input is the node's schema under `value` (`submitParameters` from
8
+ `@parcha/agentrun-dsl`, the envelope the Pi runner uses too); a review that does not accept it is answered in the same
9
+ session. Every paid request passes the runner first: the turn cap, then the host's `beforeRequest`, which receives the
10
+ node's `budget_usd` and what its session has spent, and may refuse it. Under `withRecovery` a session is keyed by its
11
+ step's `sessionId`: a delivered record is answered from the session's records with nothing run, and a session a
12
+ process died inside resumes from its records with nothing paid twice. Without a step each call is a fresh session.
13
+ A call's `tool_end` reaches `onEvent` only once its outcome is in the session's records, with the text recorded as
14
+ `result`, so a host never shows a result its store does not hold.
15
+ - `runner.stop({ drainMs })` pauses every node the runner is running: no new turn, tool call or paid request starts, the
16
+ calls in flight run on until their outcomes are recorded (for at most `drainMs`, on an adapter that drains; the Claude
17
+ Agent adapter does), and then the node fails `aborted`. Run again with the same step, it resumes from its records: a
18
+ call that finished during the pause is never run, or paid for, again.
19
+ - `AgentAdapter` is the interface a harness implements: `check` refuses a session before anything is spent (a fork it
20
+ cannot make), and `open` restores a session from its records. A session's `settleOpenTurn()` ends a turn its records
21
+ still hold open (one the person stopped), interrupted, and runs nothing, so the turn is never resumed.
22
+ - `SessionStore` is the run's store for sessions. A session is written only through a writer, and `open(key)` takes the
23
+ session: every writer opened earlier for that key is fenced from then on, and its appends are refused
24
+ (`SessionFenced`) with nothing written. The runner opens one writer per attempt and hands it to the adapter, so a
25
+ process whose lease lapsed while it was still alive cannot act in a session a newer process holds; it ends `fenced`.
26
+ - `./testing` has `memorySessionStore` and `fauxAdapter`, a scripted harness with no model and no network.
27
+ - `./kernel` is the durable kernel an adapter runs on (`openDrive` over the session's writer): an effect's intent, then
28
+ at most one outcome; after a crash a `safe` effect runs again and a `never` one is reported interrupted, never run
29
+ again. So a tool that costs money per call (a paid search) is `never`: `safe` means free to run again. A drive whose writer was fenced gets a `Restart` at its next write, so it never runs a new effect.
30
+ `followUpOnce` sends one more turn into a session that has answered, with a prompt the host names, at most once: it
31
+ is a `never` effect on its own key (`followUpKey(session, name)`), so a crash inside it leaves it interrupted, and the
32
+ turn a crash cut off is settled (`settleOpenTurn`), never resumed. Its prompt's submission has an id of its own
33
+ (`follow-up:<key>`), and the settle is named only that one, so it never withdraws a person's message. Its
34
+ `beforeRequest` goes ahead of the host's and holds the follow-up's paid requests to `reserveUsd`, counted from its
35
+ first request; an unknown spend is refused.
36
+ `followUpState(records)` tells a host, before it resumes an open turn, whether that turn is a follow-up's.
37
+ `./process` is the shared layer for harnesses that run as a child process: the native journal, the transcript, the
38
+ recovery note, the tool bridge, the built-ins' hook-pair calls, the inbox (and `waitForSubmissions`, for a harness that
39
+ waits for input while a turn runs), `zodShapeFromJsonSchema`, for harness SDKs whose tool APIs take zod, and
40
+ `settleTurn`, the transcript's end for a turn a session settles (`settleOpenTurn`).
41
+ - `./host` is the session's model proxy, provider-neutral: on the loopback interface, it admits only the session's key,
42
+ only the routes a `ProviderPolicy` lists (by exact path text, with no query or one the route lists exactly; a route
43
+ may also be answered by the proxy, or be free, like a token count), and strict JSON, has the policy shape or refuse the
44
+ body, asks the runner's `beforeRequest` before each paid request, adds the provider's credential (which never reaches
45
+ the harness), and reports each response's usage to `afterRequest`. Model spend is accounted only there, by the runner.
46
+
47
+ The runner's own bounds (a turn limit, no progress, an unknown tool, a refused fork, the host's refusal) fail a node
48
+ with `final: true`, so a recovery driver gives it no second attempt. Turns sent to a node are bounded by its turn limit,
49
+ or by `maxSends` (default 100) when the limit is null, and three turns in a row that make no paid request and deliver
50
+ nothing end it `no_progress`: a harness that never reaches its model is stopped, not sent turns forever. It imports no host: serve and any DSL host use it alike.
51
+
52
+ Code this package copies from another project is listed in `vendor/durable-harness/UPSTREAM.json` and credited in
53
+ `NOTICE`.
@@ -0,0 +1,87 @@
1
+ import { type BeforeRequest } from "./runner.js";
2
+ import type { AgentAdapter, HarnessEvent, HostTool, NodeRequest, SessionStore } from "./types.js";
3
+ /** The tool's name, as the lead calls it. */
4
+ export declare const DELEGATE_TOOL = "delegate";
5
+ /** What every delegate answers: its finding, the sources it rests on, and what it could not settle. */
6
+ export declare const DELEGATE_RESULT_SCHEMA: Readonly<{
7
+ type: "object";
8
+ properties: {
9
+ answer: {
10
+ type: string;
11
+ };
12
+ citations: {
13
+ type: string;
14
+ items: {
15
+ type: string;
16
+ properties: {
17
+ url: {
18
+ type: string;
19
+ };
20
+ title: {
21
+ type: string;
22
+ };
23
+ quote: {
24
+ type: string;
25
+ };
26
+ };
27
+ required: string[];
28
+ additionalProperties: boolean;
29
+ };
30
+ };
31
+ gaps: {
32
+ type: string;
33
+ items: {
34
+ type: string;
35
+ };
36
+ };
37
+ };
38
+ required: string[];
39
+ additionalProperties: false;
40
+ }>;
41
+ /** One named sub-agent, as the host defines it. */
42
+ export interface DelegateAgent {
43
+ /** What the lead is told this agent is for. */
44
+ description: string;
45
+ /** The child's system prompt, verbatim. */
46
+ system: string;
47
+ /** The lead's tools this agent gets, by name: a subset of `DelegateOptions.tools`. */
48
+ tools: readonly string[];
49
+ /** The models-table tier the child runs on (the node's `tier`). */
50
+ tier?: NodeRequest["tier"];
51
+ /** The share of the lead's remaining time the child may take, in (0, 1] (default 1). */
52
+ deadlineShare?: number;
53
+ }
54
+ /** A child's event: the runner's event, with the delegate call it belongs to. */
55
+ export type DelegateEvent = HarnessEvent & {
56
+ delegate: {
57
+ agent: string;
58
+ callId: string;
59
+ parentKey: string;
60
+ };
61
+ };
62
+ export interface DelegateOptions {
63
+ /** The sub-agents by name (lowercase letters, digits and dashes). */
64
+ agents: Readonly<Record<string, DelegateAgent>>;
65
+ /** The lead's tools: each agent gets the ones it names, and no others. */
66
+ tools: readonly HostTool[];
67
+ /** The harness children run on, or one per agent (an agent's own skills, say). */
68
+ adapter: AgentAdapter | ((agent: string) => AgentAdapter);
69
+ /** The run's store: each child keeps its session there, under its own key. */
70
+ store: SessionStore;
71
+ /** The lead's session key: a child's is `<parentKey>/delegate:<call id>`. */
72
+ parentKey: string;
73
+ /** The lead's check before each paid request, so one budget covers the lead and its children. */
74
+ beforeRequest?: BeforeRequest;
75
+ /** Every child event, with the delegate call it belongs to. */
76
+ onEvent?: (event: DelegateEvent) => void;
77
+ /** The lead's deadline (epoch ms), if it has one: a child's is this, or its agent's share of the time left. */
78
+ deadlineAt?: () => number | undefined;
79
+ /** A child's limit when the lead has no deadline (absent: the runner's own default, 120 s; null for none). */
80
+ timeoutMs?: number | null;
81
+ /** The most paid requests a child makes per attempt (the runner's default when absent). */
82
+ maxTurns?: number | null;
83
+ }
84
+ /** What is wrong with the delegate's definition, or nothing: checked when the tool is made. */
85
+ export declare function delegateProblems(options: Pick<DelegateOptions, "agents" | "tools">): string[];
86
+ /** The `delegate` host tool: refuses a bad definition when made. */
87
+ export declare function delegateTool(options: DelegateOptions): HostTool;