Based on AgentRun release · Synced .
@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'srunNode. A node's record arrives through an injectedsubmittool whose input is the node's schema undervalue(submitParametersfrom@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'sbeforeRequest, which receives the node'sbudget_usdand what its session has spent, and may refuse it. UnderwithRecoverya session is keyed by its step'ssessionId: 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'stool_endreachesonEventonly once its outcome is in the session's records, with the text recorded asresult, 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 mostdrainMs, on an adapter that drains; the Claude Agent adapter does), and then the node failsaborted. 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.AgentAdapteris the interface a harness implements:checkrefuses a session before anything is spent (a fork it cannot make), andopenrestores a session from its records. A session'ssettleOpenTurn()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 bysettleTurn'sunsent. 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 answersturn/startbefore 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.SessionStoreis the run's store for sessions. A session is written only through a writer, andopen(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 endsfenced../testinghasmemorySessionStoreandfauxAdapter, a scripted harness with no model and no network../kernelis the durable kernel an adapter runs on (openDriveover the session's writer): an effect's intent, then at most one outcome; after a crash asafeeffect runs again and aneverone is reported interrupted, never run again. So a tool that costs money per call (a paid search) isnever:safemeans free to run again. A drive whose writer was fenced gets aRestartat its next write, so it never runs a new effect.followUpOncesends one more turn into a session that has answered, with a prompt the host names, at most once: it is anevereffect 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. ItsbeforeRequestgoes ahead of the host's and holds the follow-up's paid requests toreserveUsd, 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../processis 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 (andwaitForSubmissions, for a harness that waits for input while a turn runs),zodShapeFromJsonSchema, for harness SDKs whose tool APIs take zod, andsettleTurn, the transcript's end for a turn a session settles (settleOpenTurn)../kernel/capis 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.beforeRequestOfis the check on the runner's booking (the model proxy's requests);createCap/openCapis the host's own ledger (decisions, priced tools);parkOf,checkRaiseandfollowUpOfdecide what a refusal and a raise do to the run../hostis the session's model proxy, provider-neutral: on the loopback interface, it admits only the session's key, only the routes aProviderPolicylists (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'sbeforeRequestbefore each paid request, adds the provider's credential (which never reaches the harness), and reports each response's usage toafterRequest. 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.