# @parcha/agentrun-kernel

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
harness adapter the way they run on Pi, durable in AgentRun's own store.

- `createHarnessRunner(adapter, options)` is exactly the interpreter's `runNode`. A node's record arrives through an
  injected `submit` tool whose input is the node's schema under `value` (`submitParameters` from
  `@parcha/agentrun-dsl`, the envelope the Pi runner uses too); a review that does not accept it is answered in the same
  session. Every paid request passes the runner first: the turn cap, then the host's `beforeRequest`, which receives the
  node's `budget_usd` and what its session has spent, and may refuse it. 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 resumes from its records with nothing paid twice. Without a step each call is a fresh session.
  A call's `tool_end` reaches `onEvent` only once its outcome is in the session's records, with the text recorded as
  `result`, so a host never shows a result its store does not hold.
- `runner.stop({ drainMs })` pauses every node the runner is running: no new turn, tool call or paid request starts, the
  calls in flight run on until their outcomes are recorded (for at most `drainMs`, on an adapter that drains; the Claude
  Agent adapter does), and then the node fails `aborted`. Run again with the same step, it resumes from its records: a
  call that finished during the pause is never run, or paid for, again.
- `AgentAdapter` is the interface a harness implements: `check` refuses a session before anything is spent (a fork it
  cannot make), and `open` restores a session from its records. A session's `settleOpenTurn()` ends a turn its records
  still hold open (one the person stopped), interrupted, and runs nothing, so the turn is never resumed. A prompt it
  stopped before the harness placed it stays in the conversation: the next prompt or follow-up carries its text first,
  with a line saying it was stopped before any work on it began. The carry is in that message's text in the session's
  state, so a crash's recovery of it, placed but never recorded by the CLI, sends the stopped words too. So does a
  prompt the harness placed and its CLI never took (the stop came between the two), carried by `settleTurn`'s `unsent`.
  Whether the CLI took it is the CLI's own record as the session keeps it, the history a resumed CLI is given:
  claude-agent's transcript, Codex's rollout (Codex answers `turn/start` before it writes the message, and the Claude
  SDK writes a message to the CLI at once, so a stop can kill either before it records it). A prompt that record lacks
  the next turn's model never saw, so carrying it never sends it twice. OpenCode marks a prompt just before it sends it,
  and a marked one is never carried. A recovery message records which placed prompts it carries before it goes, so once
  the CLI holds it they count as recorded, across a chain of recoveries. A follow-up's own prompt, named to the settle
  (`opening`), is withdrawn and never carried.
- `SessionStore` is the run's store for sessions. A session is written only through a writer, and `open(key)` takes the
  session: every writer opened earlier for that key is fenced from then on, and its appends are refused
  (`SessionFenced`) with nothing written. The runner opens one writer per attempt and hands it to the adapter, so a
  process whose lease lapsed while it was still alive cannot act in a session a newer process holds; it ends `fenced`.
- `./testing` has `memorySessionStore` and `fauxAdapter`, a scripted harness with no model and no network.
- `./kernel` is the durable kernel an adapter runs on (`openDrive` over the session's writer): an effect's intent, then
  at most one outcome; after a crash a `safe` effect runs again and a `never` one is reported interrupted, never run
  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.
  `followUpOnce` sends one more turn into a session that has answered, with a prompt the host names, at most once: it
  is a `never` effect on its own key (`followUpKey(session, name)`), so a crash inside it leaves it interrupted, and the
  turn a crash cut off is settled (`settleOpenTurn`), never resumed. Its prompt's submission has an id of its own
  (`follow-up:<key>`), and the settle is named only that one, so it never withdraws a person's message. Its
  `beforeRequest` goes ahead of the host's and holds the follow-up's paid requests to `reserveUsd`, counted from its
  first request; an unknown spend is refused.
  `followUpState(records)` tells a host, before it resumes an open turn, whether that turn is a follow-up's.
  `./process` is the shared layer for harnesses that run as a child process: the native journal, the transcript, the
  recovery note, the tool bridge, the built-ins' hook-pair calls, the inbox (and `waitForSubmissions`, for a harness that
  waits for input while a turn runs), `zodShapeFromJsonSchema`, for harness SDKs whose tool APIs take zod, and
  `settleTurn`, the transcript's end for a turn a session settles (`settleOpenTurn`).
- `./kernel/cap` is the one spend cap every harness books through, the same rule and words for each: it admits a paid step
  (a model request on any harness, a decision, a priced tool) under the run's cap at the catalog's price, tiers included,
  holds and books it, and a step that does not fit parks the run for a raise. `beforeRequestOf` is the check on the
  runner's booking (the model proxy's requests); `createCap`/`openCap` is the host's own ledger (decisions, priced tools);
  `parkOf`, `checkRaise` and `followUpOf` decide what a refusal and a raise do to the run.
- `./host` is the session's model proxy, provider-neutral: on the loopback interface, it admits only the session's key,
  only the routes a `ProviderPolicy` lists (by exact path text, with no query or one the route lists exactly; a route
  may also be answered by the proxy, or be free, like a token count), and strict JSON, has the policy shape or refuse the
  body, asks the runner's `beforeRequest` before each paid request, adds the provider's credential (which never reaches
  the harness), and reports each response's usage to `afterRequest`. Model spend is accounted only there, by the runner.

The runner's own bounds (a turn limit, no progress, an unknown tool, a refused fork, the host's refusal) fail a node
with `final: true`, so a recovery driver gives it no second attempt. Turns sent to a node are bounded by its turn limit,
or by `maxSends` (default 100) when the limit is null, and three turns in a row that make no paid request and deliver
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.

Code this package copies from another project is listed in `vendor/durable-harness/UPSTREAM.json` and credited in
`NOTICE`.
