Skip to content

Latest commit

 

History

History
734 lines (612 loc) · 45.6 KB

executors.md

File metadata and controls

734 lines (612 loc) · 45.6 KB

Executors

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

The contract

Use an executor

Pick an executor

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.

Install

npm install @ambionframework/ambion @ambionframework/journal @ambionframework/pi

Replace 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.

Options every executor takes

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.

Wire an execution

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.

Sign in

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 usage step 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

Troubleshooting

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.

Test with the scripted executor

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 executor contract

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 pass contract

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 returns failed: 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 session after the last pass. Keep the id of the vendor session there until the driver calls close. The release records { kind, id }, where kind is definition.executor.kind. A running activation with no session id records none. The room hands the id to the next activation as spec.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.

The hosting entry exports

@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.

  • describeExecutor builds the neutral half of an executor definition, which an executor kind extends with its fields. It calls each SeatExtension of extensions once, 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(), and codex() add the extension that composeExtension of @ambionframework/compose builds from compose after the extensions of the options. The compose value defaults to quickjsRuntime(). A second composeExtension in extensions brings duplicate tools named compose.
  • activationTool builds a tool whose entry receives the access of the activation: the room tools, nested, and approved. compose is such a tool. The invoke field of an activation tool runs with no activation.
  • invokeTool runs 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.
  • invokeChecked makes the same call for a harness that prepared and checked the arguments already, as Pi does.
  • checkedArguments, messageOf, and mismatchOf serve a tool that calls other tools. checkedArguments prepares and checks the arguments of a call. messageOf gives the message of a thrown value. mismatchOf says 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 prompt the driver renders

The driver renders three prompt parts. Each part depends on one thing, so an adapter places it where it caches best.

  • mechanism depends on the kernel version only. It states how a room works.
  • agent depends 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.

How an activation runs

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.

The room tools

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.tools holds the room tools that the purpose of the activation grants, then the tools of the definition. A summary activation gets the room tools alone. Each BoundTool has a name, a description, TypeBox parameters, and run(args, call). call is 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 the ToolContext of one call of a definition tool, for an executor that hosts the tools of the definition itself. It carries the view's deadline: 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 BoundTool converts nothing.
  • The harness runs a batch in turn when a tool sets executionMode to sequential.
  • The harness gives the tool onUpdate, and the abort signal of the pass.
  • The harness keeps details of the result. A BoundTool result has no details.

Pi builds each tool from its AmbionTool, and toolContext gives each call the context that the driver gives it.

The step vocabulary

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.

Exchange continuity

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.

Failure classification

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.

The harness matrix

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.

How to write an adapter

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.

  1. Implement ActivationOpener and RunningActivation. 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.
  2. Place the prompt. Put pass.mechanism and pass.agent where the harness caches them, and send the text of pass.record(). The prompt the driver renders states the parts.
  3. Host the tools. Adapt pass.tools to 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 its AmbionTool, as Pi does. The room tools states the commit key and the room answers.
  4. Record the steps you own. Call trace.record of the activation for thinking, text, tool_call, tool_result, session when the harness reports its session, and usage. The driver records pass, room, and end. The driver records steer, and it raises the tool events from the steps.
  5. Report what the model consumed. Call read(range) when the model consumes a range, and delivered(call) when a tool result reaches it, and on nothing earlier. How an activation runs states the events. Declare steer only when the harness takes a line into a live pass. The driver rule sets consumed.
  6. Classify every failure. Sort it into permanent and transient with classifyCause. Throw PermanentError for a fault that a retry cannot clear, such as a model that the registry does not hold, and call failedPass for a thrown fault of the executor. Failure classification states the shared rule; bring the source of a status that the executor kind has.
  7. 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.
  8. 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 offers claude() and claudeExecution(). Build the value with localExecution in the second function. The host supplies it through the execution option of the room or runtime. The Claude guide shows the wiring.

The conformance suite

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.