An executor runs one agent's model loop for one activation. The room and the driver own the record, the lease, the rules, and the state of each activation. An executor owns one harness: the model call, where the tools run, and the steps it reports. The README holds the positioning.
This page has two parts. Use an executor serves a host that picks an executor and wires it into a room. The contract serves a reader who writes an adapter or needs the rules between the driver and an executor: the pass, the room tools, exchange continuity, failure classification, the step vocabulary, and the way to write an adapter. The Pi guide, the Claude guide, and the Codex guide hold only what differs per adapter.
Use an executor
- Pick an executor
- Install
- Options every executor takes
- Wire an execution
- Sign in
- Troubleshooting
- Test with the scripted executor
The contract
- The executor contract
- The pass contract
- The hosting entry exports
- The prompt the driver renders
- How an activation runs
- The room tools
- The step vocabulary
- Exchange continuity
- Failure classification
- The harness matrix
- How to write an adapter
- The conformance suite
Three executor kinds ship. A room can run seats of several kinds.
| Kind | Package | The loop runs in | Pick it for |
|---|---|---|---|
| Pi | @ambionframework/pi |
The host process, on the Pi harness | Any provider of the Pi registry, tools in process, seats apart from the room, scripted streams |
| Claude | @ambionframework/claude |
A Claude Code child process of the host | The Claude Code loop on a Claude model |
| Codex | @ambionframework/codex |
A codex app-server child process |
The Codex loop on an OpenAI key or a ChatGPT subscription |
A seat of any kind has no tool beyond the room tools and your tools. Files
and a shell come from the workspace tools in bundles.
Trust states what each harness exposes.
Claude and Codex spawn a process, so the host must be able to run one. The
Cloudflare adapter builds its seats on Pi.
npm install @ambionframework/ambion @ambionframework/journal @ambionframework/piReplace pi with claude or codex for another kind. Every package
needs Node 22.19 or newer. The packages install from npmjs with no token. A
dev build of main installs from GitHub Packages; see
Toolchain. The kernel imports no
model library, so each package brings its own harness at an exact version.
pi(), claude(), and codex() return a frozen executor of their kind.
The kernel validates the shared fields, which are ExecutorBaseOptions. Each
adapter also requires model and adds options of its own, such as compose.
The adapter page lists them.
| Option | Required | Default | Meaning |
|---|---|---|---|
instructions |
Yes | None | The private guidance of the agent. |
tools |
No | None | The tools of the agent, from defineTool. They run in the host process. |
bundles |
No | None | Tool bundles. Their guidance joins the prompt after the respond policy. |
compose |
No | quickjsRuntime() |
The compose and describe tools of the seat: a runtime and optional limits (Compose). |
extensions |
No | None | Extensions of the seat, which the executor runs before compose. Each adds tools and guidance. |
respondPolicy |
No | DEFAULT_RESPOND_POLICY |
The respond policy. It replaces the default. |
summaryPolicy |
No | DEFAULT_SUMMARY_POLICY |
The summary policy. It replaces the default. |
activationTokenLimit |
No | The record the room serves | The token limit of the record one activation reads. A positive integer. |
estimateTokens |
No | Math.ceil(text.length / 4) |
A function that counts the tokens of a text. It needs the limit. |
estimateTokens is a function that the room runs. The room windows the
record with it. The definition throws when estimateTokens has no
activationTokenLimit, when the limit is not a positive integer, and when
estimateTokens is not a function. History and limits
states the rule.
x() defines the executor of one agent. xExecution() gives a room or a
runtime the services that run it. Here x is pi, claude, or codex.
Pass the execution as execution to startRoom, resumeRoom, or
createRuntime. A room with several kinds takes a list, such as
[piExecution(), claudeExecution()]. A host with no special need calls
xExecution() with no argument.
xExecution(options) configures the services of one kind. Each adapter
page lists its options: the binary, the environment, the place of the
sessions, and the sign-in. The runtime supplies the clock, the call limits,
the trace limits, and the logger. The executor contract
states how a room resolves an execution.
A seat reads its credential from the host. The adapter page names the variable or the stored sign-in. These rules hold for every kind:
- A refused sign-in is a permanent failure. The room does not retry it.
- A subscription has its own usage limit. A limit that the provider reports is a permanent failure.
- The cost on a
usagestep is notional for a subscription. A subscription bills nothing per token. Codex reports no cost. - A provider may restrict a consumer subscription outside its own clients. Read the terms first. They are the choice of the account owner.
| Kind | Key | Subscription |
|---|---|---|
| Pi | <PROVIDER>_API_KEY |
loginPi once, then piExecution({ credentials }) |
| Claude | ANTHROPIC_API_KEY |
claude setup-token, then CLAUDE_CODE_OAUTH_TOKEN |
| Codex | CODEX_API_KEY |
codex login, linked into the Codex home of the seats |
These symptoms hold for every kind. The adapter page lists the rest.
| Symptom | Cause |
|---|---|
Each seat fails at once with no_execution |
No supplied execution serves the kind of the seat. Pass the xExecution() of the kind to the room or runtime. |
Cannot run an executor of kind 'a': this seat needs 'b'. |
A seat of kind a reached the executor of kind b through an execution with no kind. Pass the execution of each kind. |
| The seat is abandoned after one attempt | A permanent failure. Read the error event. Check the credential, the credit, and the usage limit. |
An agent estimateTokens needs an activationTokenLimit. |
estimateTokens is set with no limit. |
| The agent never speaks | Silence is legal. Pass a logger to createRuntime and read the thinking and the tool calls there. |
A say returns Not delivered: the room moved |
The freshness rule refused a say against newer record. The model reads the new messages, then says its message again unless they already say it or make it unnecessary. |
A steer shows consumed: false |
The pass ended before the harness took the line. The next delta carries it. |
The activation ends with stop: 'length' |
The last model message hit a length limit. This is no failure. Shorten the record with activationTokenLimit. |
| A resumed seat starts a fresh session | The store lost the session: a new disk, a changed directory, or an evicted seat. The room record holds the exchange, and the release records the new id. |
scripted(script) runs a room with no model, no key, and no process.
@ambionframework/ambion/testing exports it. The execution has no kind, so
it serves every seat. A script is a function of the step, the seat, and the
request count. It returns a reply built with say, callTool, schedule,
seat, spend, or quiet, and byAgent routes a script by seat. settled(room)
waits for the room, and fakeClock moves time by hand.
The scripted executor also reads the room answer of a commit. A script
branches on a short answer, such as delivered, missed, or stale: <why>.
The result holds only the text that a model reads, and the scripted executor
makes no call to the room. The driver keeps the answer beside each result for
the scripted executor alone. The hosting entry does not export it.
An own tool of the seat can throw. The scripted executor records the
tool_result step with the error, and the script reads the message in
step.results as the text of the result. The activation goes on, as it does
with Pi, Claude, and Codex. A script that throws a ScriptedFailure ends the
pass with the cause it names.
The scripted executor proves the room. A scripted harness proves the
adapter. Each adapter runs its real harness against a stand-in for the
provider: Pi through a scripted stream (Pi), Claude through a
fake executable (Claude), and Codex through a scripted
model (Codex). piExecutorFixture from
@ambionframework/pi/testing and claudeExecutorFixture from
@ambionframework/claude/testing each give executorConformance a fixture
(The conformance suite). Codex keeps its fixture in
its tests.
A scripted run shows nothing about the provider. It cannot show real payloads, price tables, token counts, the length of a real context, or what a real model says. The live tier of each package covers those claims and costs money.
The host configures execution before it starts the room. An Execution
is a value that an executor package builds, such as piExecution() from
@ambionframework/pi. It names the kind of executor that it serves. The
runtime gives it the clock, the storage, the call retry policy, the trace
limits, and the logger. It returns a connector. The room supplies the
captured agent definition and receives an execution port. It does not
construct a model runner.
One router serves every seat. createRuntime takes an execution for
every room of the runtime. startRoom and resumeRoom take an execution
for one room run. Each takes one execution or a list, such as
[piExecution(), claudeExecution()]. A seat runs on the first execution of
the room that serves its kind, then on the first of the runtime. An
execution with no kind serves every kind. A seat that no execution serves
fails at once with a no_execution error,
and the failure is permanent. A room with no execution still runs its
people and its record.
localExecution(kind, build) makes one execution of a kind. An executor
package uses it to build a value, such as piExecution(options). The host
supplies that value to a room or runtime. Importing a package or building an
execution does not configure a room.
export function localExecution(
kind: string,
build: (host: ExecutionHost) => (request: ConnectorRequest) => ActivationOpener,
): Execution<AgentRunner>;The port of a local execution is an AgentRunner in this process.
build runs once for each connector. Each room builds the connector of
a supplied execution when the first seat that the execution serves connects.
startRoom does not build it. The function that build returns builds the
opener of one seat. Every seat keeps the trace limits and the logger
of the host. The runner receives a plain RoomProtocol facade with
view, commit, and lease. The facade gives no access to room lifecycle
methods.
A remote host writes an execution whose port crosses the boundary. Its
connector returns an AgentPort that carries wake, steer, and cut to
the host that runs the seat. That host calls the execution of its own
runtime for the seat, and the seat reaches back through the same boundary.
Only protocol data crosses. The room object of @ambionframework/cloudflare
runs one such execution, rpcExecution. The seat object connects the Pi
execution once, and awaits AgentRunner.run inside its alarm.
AgentRunner is the driver. It owns the lease, its renewal, and the
wake queue. It knows no model and no provider. For each activation it opens
one ActivationState over the opener of the seat, and the state opens one
RunningActivation. The driver asks the room for the windowed view and runs
one pass after another until the activation stops.
The driver owns the state of an activation. It causes or observes each fact of it, so no executor keeps a copy.
| The driver keeps | How |
|---|---|
readThrough |
It joins the ranges that the executor reports read, and the positions that a say confirms. |
| The cut | It aborts the signal of the activation. A cut activation runs no other pass. |
| The refresh test | It runs another pass when the record stands past readThrough. |
| The room tools and their binding | It binds them to the activation, and hands them over as values. |
| The prompt | It renders mechanism, agent, and the record of each pass. |
| The tool events | It raises them from the tool_call and tool_result steps. |
The error event |
It raises one event for each failed pass. |
| The resume token | It reads spec.resume when it names the kind of the executor. |
An executor keeps its harness alone. It maps the harness events to steps, resumes a vendor session, hosts the tools, and reports the signal that the model consumed input.
An ActivationOpener is a function of the activation. It takes an
ExecutorActivation and returns a RunningActivation. contract.ts holds
the fields of ExecutorActivation, Pass, and RunningActivation.
A running activation follows these rules. The driver reads it at fixed points.
- A pass that the cut ends reports no failure. The cut aborts
signal, and the pass returnsfailed: false. - A pass that ignores the cut keeps running. The driver stops waiting for
it, and the seat can run its next activation. The seat answers the room
calls of the old pass as stale, and the room refuses them once the lease
ends. The tool effects of the old pass still land, such as bash, git, a
process, or a network call. An executor must honor
signal. - The driver reads
sessionafter the last pass. Keep the id of the vendor session there until the driver callsclose. The release records{ kind, id }, wherekindisdefinition.executor.kind. A running activation with nosessionid records none. The room hands the id to the next activation asspec.resume, and never reads it. - Every pass holds the same tool values. The driver binds the tools on the first pass, so an adapter can host them once for the activation.
The first pass receives the view. A later pass receives a delta. The
view is the whole windowed record. A delta holds the fresh view and
after, the position the activation had read through. A pass that throws
is a failed pass, and its cause follows the
error.
Freshness bounds correctness. Steering is a capability. The driver
reads readThrough to renew the lease and to release it. A commit that
finds a newer message in the record fails as missed, and the seat reads
again. An executor that cannot steer omits steer. The next pass rereads
the record, so a dropped steer is not lost. Durability
states the commit-freshness promise.
@ambionframework/ambion/hosting exports the execution protocol. The
main entry names none of it. Journal events and projected lease state stay
internal. Participant views omit sessionId. The hosting.ts file lists the
protocol types with their TSDoc.
describeExecutorbuilds the neutral half of an executor definition, which an executor kind extends with its fields. It calls eachSeatExtensionofextensionsonce, with the tools of the seat, its bundles, and the room tools. It appends the tools and the guidance that each extension returns. With no extension, it adds none. An extension reads the tools of the seat, not the tools of another extension.pi(),claude(), andcodex()add the extension thatcomposeExtensionof@ambionframework/composebuilds fromcomposeafter the extensions of the options. Thecomposevalue defaults toquickjsRuntime(). A secondcomposeExtensioninextensionsbrings duplicate tools namedcompose.activationToolbuilds a tool whose entry receives the access of the activation: the room tools,nested, andapproved.composeis such a tool. Theinvokefield of an activation tool runs with no activation.invokeToolruns one direct call of a tool: it prepares and checks the arguments, then invokes. It hands the access of the activation to an activation tool alone.invokeCheckedmakes the same call for a harness that prepared and checked the arguments already, as Pi does.checkedArguments,messageOf, andmismatchOfserve a tool that calls other tools.checkedArgumentsprepares and checks the arguments of a call.messageOfgives the message of a thrown value.mismatchOfsays what a schema refuses.
RoomProtocol.view(activation, message?) takes no range. The room
serves the record windowed to its cap and to the token limit of the seat,
so a seat never pages the record. With message, the view holds that one
message when the purpose may read it; recall reads a message below the
window this way. The hosting entry exports no ViewRange, and the
context of a view holds omitted and no earliest.
The driver renders three prompt parts. Each part depends on one thing, so an adapter places it where it caches best.
mechanismdepends on the kernel version only. It states how a room works.agentdepends on the definition and the purpose. It holds the name, the respond policy, the identity, and the instructions. A closing seat reads its summary duties here.record()depends on the activation. On the first pass it holds the clock, the room, the roster, the record, the reminders of the tool bundles, and the ask line. A summarize activation gets no reminder.
A later pass reads the delta. record() marks each message beyond
after with the [new] prefix, and gives undefined when nothing is new.
The driver then counts the view read. The runner marks each steered line with
the same [new] prefix, and an executor sends the line unchanged. The driver
renders each room refusal for the model.
A resumed session reads the delta on its first pass. record(after)
takes the position that the vendor session read through. The first pass
of a respond activation then reads the reminders, the scheduled says, and
the messages beyond after. Pi passes it; Claude and Codex read the whole
view.
A definition can replace each policy. The main entry exports
DEFAULT_RESPOND_POLICY and DEFAULT_SUMMARY_POLICY. An executor takes a respondPolicy
option and a summaryPolicy option. respondPolicy replaces the whole respond
policy: when to speak, how to read an arrival, how to hand work to a
colleague, and the closing reminder of the ask line. summaryPolicy replaces
the summary duties and the advice of the summary ask line. The kernel keeps
the mechanism: the room, the roster, the record, the opening message, the
range of a summary, and the tools. Tool bundle guidance stays in the
guidance field and follows the respond policy.
The driver resolves the reminders of the bundles once for each respond
activation, when record() has something to send. Each reminder has 5
seconds to answer, and at that bound the driver aborts the signal that it
passed to the reminder. A reminder that throws, rejects, gives blank text,
or answers late gives no text. The driver does not cut a reminder, so the
bundle bounds the length of its own text.
The prompt the driver renders states the parts. The adapter page names the placement for its executor kind.
readThrough advances only when the model has consumed a message. The
driver keeps it. The table below holds for every executor kind. An adapter page
names the signal it reads for the first and the last events.
| Event | Who tells the driver | What moves |
|---|---|---|
| The model reads a prompt, a delta, or a steered line | The executor: read |
readThrough moves to the position of that message. |
The room accepts an ordinary say |
The say tool | readThrough moves to the position the say confirms. |
The room accepts a schedule with no unread |
The schedule tool | readThrough moves to the position of the scheduled say. |
The tool result of a missed say reaches the model |
The executor: delivered |
readThrough moves to the last of the messages the result carries. |
The tool result of a schedule reaches the model |
The executor: delivered |
readThrough moves to the scheduled say, past its unread. |
A steered line moves the position only when the record before it is
already read. A message that lands out of order does not advance
readThrough until the gap closes. The driver holds the range, and it joins
the range once the gap closes. Of two held ranges through one position,
the driver keeps the range that starts lower. The order in which the ranges
arrive does not change the position.
The driver decides on another pass. It runs one when the record stands
past readThrough and the activation was not cut.
The driver records the steer step of every line, with consumed.
consumed: true marks a line the pass delivers. consumed: false marks a
line that the pass does not use before it ends, and the next delta carries
it. The driver decides by the moment the line lands:
| The line lands | The driver records |
|---|---|
| Before the first pass | Nothing yet. The line waits for the first pass, and then follows the rules below. |
| Before a first pass that never runs | consumed: false when the pass would have started: a cut, a view of another seat, or a failed claim. |
| Between two passes | consumed: false at once. |
| In a pass, and the view of the pass holds the line | consumed: true at once. |
In a pass, and the executor has no steer |
consumed: false at once. |
In a pass, and the executor has steer |
consumed: true when the executor calls read for the line, or consumed: false when the pass ends first. |
In a pass, and steer throws |
consumed: false at once, unless the executor read the line first. |
The driver calls steer at any moment after it calls pass and before that
pass settles. That includes the moment before the body of pass reaches its
first await. The executor holds a line that its harness cannot take yet,
delivers it when the harness can, and drops what it holds when pass
settles. It calls read({ after, through: seq }) when the model consumes
the line, with the after and the seq that steer received. It records
no steer step. The guide of each executor kind states the moment its executor calls
read, because the moment differs by kind.
The room applies the activation token limit. It keeps the newest
messages that fit activationTokenLimit, and keeps the open exchange
whole, inside the view it serves. The limit counts record text through the
function that estimateTokens holds, or the default estimate. It
does not count the system prompt, the tool schemas, or the model output. It
does not compare with the context window of the model. The rule holds for
every executor kind, and one view is one call over the wire.
History and limits states the rule.
Definitions and tools states which tools a respond activation receives and which tools a summary activation receives.
The driver binds the tools of the activation once, in a form that names no harness. Each executor kind adapts them to its own tool shape.
pass.toolsholds the room tools that the purpose of the activation grants, then the tools of the definition. A summary activation gets the room tools alone. EachBoundToolhas aname, adescription, TypeBoxparameters, andrun(args, call).callis the id of the tool call, and the commit takes it as its key. The driver binds each room tool to the read position and the cut of the activation. A call of a tool of the definition reads the view of the pass that runs it.toolContext(agent, view, call, signal, onUpdate?)builds theToolContextof one call of a definition tool, for an executor that hosts the tools of the definition itself. It carries the view'sdeadline: when the room ends the activation, on the wall clock.
The result of a room tool holds the content that the model reads.
isError marks an error result. terminate marks an activation that has
nothing more to do: an unknown or stale answer, or the last answer of a
summary activation. carriesRecord marks content that holds lines of the
record the model has not read, so a compose call shows it in its own
result.
say commits a said intent. It carries readThrough and takes the
tool call id as its commit key. It accepts text, to, and refs. The
result names the message, as said #41 or said #41 to priya, so the
agent can cite it. seat and unseat give seated surveyor (#42), and a
seating the record already holds gives surveyor is already seated. The result tells the agent to read the
mark in the roster. A seat marked "named only" gets the request through say
with to set to the name. A seat with no mark, or marked "watches arrivals",
already has it.
schedule commits a said intent with delaySeconds. The intent goes to the
seat itself. It carries readThrough, and the room takes it at any
position. It accepts delaySeconds, text, and refs. The result names the say
as #<seq> and gives the due time, and it lists the unread messages of
the answer.
seat and unseat commit a seating intent, keyed on the tool call
id. The room offers seat only when the reserve held an agent as the room
composed, and offers neither tool when the host started the room with
seating: false. The tool list of a seat does not change between its
activations.
recall reads and commits nothing. For each distinct ref of its
room, it calls view(id, seq). The view of one message applies no window
and folds no summarised range, so it reaches a message below the window. The
tool calls neither acknowledgeThrough nor resultExpected, and every
executor kind reports it as a tool event.
The room answer tells the adapter what to do:
| Room answer | What the adapter does |
|---|---|
committed or unchanged |
The adapter delivers the result. |
refused |
The adapter raises the room message as a tool error. |
missed |
The adapter raises a tool error that lists the new messages. |
unknown or stale |
The adapter cuts the activation. The message may already stand on the record. |
A seat without the authority of its activation hears stale. The
authority is a live lease and the grant that the record gives the
activation id. The grant holds only for a seat on the roster. view,
commit, and a lease release check the authority when the seat asks, and
commit and a release check it again where the write lands. The answer
names the reason: the lease ended or the activation has no room grant.
A claim or a renewal that the room refuses answers the lease ended.
A room tool that commits an entry raises no tool event. say,
schedule, seat, unseat, and dismiss raise no tool_call event
and no tool_result event. The entry that each one commits reaches
the host as a message event, and a tool event would report the same
fact a second time. recall commits nothing, so it raises tool events, as
every tool of the definition does.
The driver raises the tool events from the steps. It pairs each
tool_call step with the tool_result step of the same call id. An
executor records the steps, and raises no tool event.
The executor hosts the tools. Pi runs them in its harness, Claude in an
MCP server in the process, and Codex as dynamic tools of its thread. A harness
that cannot see the id of a call takes it with callId(tool): the oldest tool_call step of that tool that
no call took yet, or a fresh id when none waits. A step with a parent is
never that step. A nested bash call of compose cannot give its id to a
direct bash call of the same batch. A tool_result step ends
its call, so the driver drops the id of that call. Each adapter page names
the transport.
Pi hosts the room tools of pass.tools, and builds the tools of the
definition itself. Claude and Codex host all of pass.tools. A
BoundTool does not carry what the Pi harness does with a tool of the
definition:
- The harness converts a primitive argument to the type of the schema
before it checks the call. A
BoundToolconverts nothing. - The harness runs a batch in turn when a tool sets
executionModetosequential. - The harness gives the tool
onUpdate, and the abort signal of the pass. - The harness keeps
detailsof the result. ABoundToolresult has nodetails.
Pi builds each tool from its AmbionTool, and toolContext gives each call
the context that the driver gives it.
A step is one thing an activation did. The vocabulary has thirteen kinds,
and every executor kind shares it. Traces holds the
kinds, the order of the steps, the policy, and the
limits. The driver records pass, input, room, steer, and end. An
executor records the rest through the record of its activation.
A seat keeps its vendor session for the length of one exchange. A seat often works in more than one activation of an exchange: it speaks, its lease ends, another participant answers, and the room wakes it again. The second activation continues the session of the first. It keeps the reasoning, the tool calls and the tool results that the record does not hold. The first activation of a seat in each exchange starts fresh.
The release records the session, and the room hands it back. The
release records { kind, id } on the ended entry. The room gives
the next activation of the same seat in the same exchange the latest such
session as spec.resume. A summary activation gets the session of the
exchange it summarizes. The room never reads the id. There is no option:
every executor works this way.
An executor resumes only the session that pass.resumeId names. The driver
sets pass.resumeId only when spec.resume names the kind of the
executor. With no pass.resumeId the executor starts a fresh session.
Pi keeps each session of a seat apart, so the
open exchange runs beside the summary of the exchange before it.
The session is a cache, and its persistence is best effort. The
harness keeps the session where it keeps it. On Node, Pi, Claude and Codex
keep it on the local disk. A Pi seat with sessions: 'memory', and a
Cloudflare seat, keep their Pi sessions in memory. A new disk, a host with no disk,
or an evicted seat object loses the session. The next activation then
starts fresh from the room record. The activation does not fail, and the
release records the new id. The record is the only state the room
promises to keep.
Each adapter keeps its own id, store, and fallback.
| Kind | The recorded id | Where the session lives | First input of a resumed activation | Fresh start when |
|---|---|---|---|---|
| Pi | The activation that began the session | A JSONL storage under sessionDir, or memory |
The delta | The store cannot open or read the session, or the harness cannot restore it |
| Claude | The id of the SDK session | The config home of the seat | The whole view, after the restated seat part | The SDK cannot resume the id |
| Codex | The id of the thread | The sessions directory of the Codex home |
The view alone, on thread/resume |
thread/resume fails, or the bound tools differ from the tools of the thread |
The adapter pages hold the detail: Pi, Claude, and Codex.
Freshness governs speech in a kept session. A kept session does not let a seat commit over a record it has not read. Durability owns the journal field and the body schemas.
The executor sorts a failure into permanent and transient. A
permanent failure ends the activation in one attempt. A transient failure
retries to the cap of the room.
Durability states what the
room does with the cause.
| Failure | Cause |
|---|---|
| An error text that names a credit, a quota, a usage limit, a credential, a login, or a permission refusal | permanent |
| A status of 400, 401, 402, 403, 404, 405, or 422 | permanent |
| An executor fault that a retry cannot clear, such as a model that the registry does not hold | permanent |
| Every other error of the executor, such as a lost room call or a lost process | transient |
| Every other failure | transient |
One classifier serves every executor kind. classifyCause({ text, status })
from @ambionframework/ambion/hosting holds the text set of every provider
that a shipped executor kind reaches: credit, billing, quota, usage limit,
authentication, permission, an invalid request, an invalid API key, an
x-api-key header, an unauthorized reply, a missing login, and an expired
sign-in. OpenAI sends a spent quota with a 429, and only the text tells it
from a rate limit. The PERMANENT_TEXT constant in
packages/ambion/src/execution/failure.ts holds the exact patterns.
A status decides the cause when no text matches. An uncertain failure is transient, so the room retries it.
One rule turns a thrown error into a failed pass. failedPass(thrown)
from @ambionframework/ambion/hosting builds the failed PassResult. The
cause is permanent for a PermanentError and transient for every other
value. The rule reads the name of the error, so a second copy of the
package gives the same cause. The result always carries error. The driver
calls it when a session throws, and an executor calls it for a fault of its
own. A fault that the retry meets again, because the retry runs the same
configuration, is a PermanentError.
The message of a failure names the provider's words. A provider error
often arrives as a status and a JSON body. providerMessage from
@ambionframework/ambion/hosting makes it 400 invalid_request_error: <message> (request <id>), and leaves another text as it is. The Pi, Claude,
and Codex executors put this text in the pass result. Classify the cause
from the original text.
A length stop is no failure. The pass reports stop: 'length'.
The driver raises the error event once. A failed pass, or a room call
that the driver cannot recover from, raises one error event with its
cause. The event carries the error of the pass result, or an error built
from its message. No executor raises one.
Each executor kind brings its own source of a status. The cause of a failure that the adapter raises itself differs too.
| Kind | Reads a status from | Other permanent causes |
|---|---|---|
| Pi | The last provider diagnostic that holds a status | An unknown model, and the harness reason no_model |
| Claude | api_error_status of a failed result |
error_max_budget_usd, and an executable older than the version floor |
| Codex | httpStatusCode in the codexErrorInfo of a failed turn |
A missing binary, a model with no catalog entry, and a login that Codex refuses |
Free text never gives a status, because a rate limit names a token count that reads like one. The adapter pages hold the detail: Pi, Claude, and Codex.
All three shipped kinds take a steer into a live pass. Pi takes it as a
queued write, Claude on the SDK user echo, and Codex on the turn/steer
echo.
An executor that cannot steer still passes. Its readThrough advances at
the pass boundary, and the driver holds a steer for the next pass. The
driver records that line as consumed: false. What a
harness remembers between activations is in Trust.
An adapter is a package that builds an Execution and an executor for one
executor kind. @ambionframework/claude is the worked example, and
@ambionframework/pi is the second.
- Implement
ActivationOpenerandRunningActivation. The opener is a function that takes the activation and returns a running activation. Keep the model loop for one activation inside the running activation. The driver records the harness session under the executor kind of the seat. - Place the prompt. Put
pass.mechanismandpass.agentwhere the harness caches them, and send the text ofpass.record(). The prompt the driver renders states the parts. - Host the tools. Adapt
pass.toolsto the form the harness needs, and run them where the harness reaches them. A harness that does more with a tool of the definition can build it from itsAmbionTool, as Pi does. The room tools states the commit key and the room answers. - Record the steps you own. Call
trace.recordof the activation forthinking,text,tool_call,tool_result,sessionwhen the harness reports its session, andusage. The driver recordspass,room, andend. The driver recordssteer, and it raises the tool events from the steps. - Report what the model consumed. Call
read(range)when the model consumes a range, anddelivered(call)when a tool result reaches it, and on nothing earlier. How an activation runs states the events. Declaresteeronly when the harness takes a line into a live pass. The driver rule setsconsumed. - Classify every failure. Sort it into
permanentandtransientwithclassifyCause. ThrowPermanentErrorfor a fault that a retry cannot clear, such as a model that the registry does not hold, and callfailedPassfor a thrown fault of the executor. Failure classification states the shared rule; bring the source of a status that the executor kind has. - Record a session, and resume only the one the pass names. Exchange continuity states the recorded session and the fresh start. A harness with no session records none.
- Wrap the executor in an
Execution. Export a function that defines the executor of an agent and a function that gives the host its execution. Claude offersclaude()andclaudeExecution(). Build the value withlocalExecutionin the second function. The host supplies it through theexecutionoption of the room or runtime. The Claude guide shows the wiring.
The suite is the acceptance gate for an adapter.
@ambionframework/ambion/conformance exports executorConformance. It
plays the driver and the room for one executor, runs the executor through
the real driver over a scripted room, and checks the room calls and the
steps the logger receives. It checks nothing an executor says beyond its
neutral plans.
An adapter supplies an ExecutorFixture. open(plan, definition) builds
the executor for one ExecutorPlan, using a fake model or a fake
executable. can is an ExecutorCapabilities value with steer, usage,
permanentFailure, and memory. The suite drops each case that a false capability
gates. close(report) runs after each case. The report holds the name of
the case, every room call with its answer, and every step the logger
received.
The suite checks the pass contract. A failed activation raises exactly
one error event, and a say raises no tool event.