Bridge
A bridge is the program through which hosts reach an agent that works in a sandbox, such as opencode, Codex, or Claude Code. It is all the sandbox exposes of the agent: it listens on a port of its own, behind a token that only hosts know, and runs the agent where nothing outside the sandbox reaches it.
A bridge is named by its BridgeId, which hosts derive from everything they launch it with, and
serves every session that hosts start with those settings. A host finds the bridge by its id, in
the listening.json the bridge writes in its state directory, and launches it when none answers
there; a host with other settings runs a bridge of its own beside it. A bridge exits once it has
been idle for a while, or once another bridge of its id has taken its place.
The bridge runs each turn itself, so that a turn goes on when a host’s connection drops or the
host releases the session, until it finishes or a host aborts it. It numbers each turn’s parts
and keeps them, so that a host reads them from any number: again after its connection drops, or
to continue a turn that another host suspended. The agent behind it needs to do neither. A turn
the bridge did not finish, as one it ran when it died, it continues when a host starts it again,
by prompting the agent with Continue. in the conversation that holds the turn.
An agent’s adapter implements Driver, which drives the agent behind a bridge, and
BridgeServer.serve serves the driver in the sandbox. The adapter’s Agent.Agent, which hosts
run, reaches it with BridgeClient.createSession. Rpcs is the protocol between them, over a
WebSocket whose host presents the token as Authorization: Bearer <token> when it connects.
Since v0.1.0
Constants
Section titled “Constants”listeningFile
Section titled “listeningFile”The name of the file in a bridge’s state directory that Listening describes.
Signature
declare const listeningFile: "listening.json"Since v0.1.0
protocolVersion
Section titled “protocolVersion”The version of Rpcs that this package speaks.
Details
Every bridge’s id includes it, so a host only ever reaches a bridge of its own version. Bump it when hosts or bridges of the version before could not read a change to the protocol.
Signature
declare const protocolVersion: 2Since v0.1.0
Models
Section titled “Models”Driver (interface)
Section titled “Driver (interface)”What an adapter implements to run its agent behind a bridge: it drives the agent in the sandbox, starting its sessions and running their turns, while the bridge keeps the turns, so that a driver needs neither to suspend nor to continue one.
Details
BridgeServer.serve serves it.
Example (A driver over an agent’s own session API)
import { Bridge, StreamPart } from "@amends-dev/harness"import { Effect, Stream } from "effect"
// What the adapter has of its agent in the sandbox: a client of the agent's own API.declare const agent: { readonly createSession: (directory: string) => Effect.Effect<string> readonly prompt: (sessionId: string, text: string) => Stream.Stream<StreamPart.StreamPart> readonly deleteSession: (sessionId: string) => Effect.Effect<void>}
const driver: Bridge.Driver = { id: "my-agent", createSession: (options) => Effect.gen(function* () { // A session the bridge starts again finds the agent's own session in `agentState`. const kept = typeof options.agentState === "string" ? options.agentState : undefined const sessionId = kept ?? (yield* agent.createSession(options.directory)) const session: Bridge.DriverSession = { keptConversation: kept !== undefined, promptTurn: ({ prompt }) => agent.prompt(sessionId, JSON.stringify(prompt.content)), submitToolResult: () => Effect.void, approve: () => Effect.void, compact: () => Effect.die("my-agent compacts only by itself"), state: Effect.succeed(sessionId), destroy: agent.deleteSession(sessionId) } return session })}Signature
export interface Driver { /** Names the agent it drives, such as `opencode`, as the agent's `Agent.Agent` for hosts does. */ readonly id: string
/** * Creates a session: a new one, or one created again from its `state`. The session runs until its * scope closes, when a host stops or destroys it, or the bridge exits. */ readonly createSession: ( options: CreateSessionOptions ) => Effect.Effect<DriverSession, HarnessError.HarnessError, Scope.Scope>}Since v0.1.0
DriverSession (interface)
Section titled “DriverSession (interface)”A session a driver started. The bridge runs one turn of it at a time, keeps its turns’ parts, and ends it by closing its scope.
Signature
export interface DriverSession { /** * Whether the session started in the conversation the agent kept for it, which `agentState` * names, rather than from `history`, as it does when it lost that conversation or had none. The * bridge continues a turn it did not finish only in a conversation the agent kept. */ readonly keptConversation: boolean
/** Runs a turn, streaming its parts until `finish`. Interrupting the stream aborts the turn. */ readonly promptTurn: (options: TurnOptions) => Stream.Stream<StreamPart.StreamPart, HarnessError.HarnessError>
readonly submitToolResult: Agent.AgentSession["submitToolResult"]
readonly approve: Agent.AgentSession["approve"]
readonly compact: Agent.AgentSession["compact"]
/** * What `createSession` takes as `agentState` to start the session again, as a restarted bridge * does. */ readonly state: Effect.Effect<Schema.Json, HarnessError.HarnessError>
/** Deletes what the agent keeps of the session. Its scope closes afterwards. */ readonly destroy: Effect.Effect<void, HarnessError.HarnessError>}Since v0.1.0
Options
Section titled “Options”CreateSessionOptions (interface)
Section titled “CreateSessionOptions (interface)”What a driver starts a session from: its id, its directory in the sandbox, the conversation so far, and the state the driver’s session last returned.
Signature
export interface CreateSessionOptions { readonly id: Agent.SessionId
/** The absolute directory the session works in. It exists before `createSession`. */ readonly directory: string
/** * The conversation so far, empty for a new session. An agent given `agentState` has it already; * any other loads it before its first turn. */ readonly history: Prompt.Prompt
/** What the session's `state` returned when it last ran on this agent. */ readonly agentState?: Schema.Json | undefined}Since v0.1.0
Protocols
Section titled “Protocols”Answers which agent the bridge runs, and which bridge it is, for a host to check that it reached the bridge it looked up: a port another bridge listened on may be another’s since.
Signature
declare const Hello: Rpc.Rpc< "Hello", Schema.Void, Schema.Struct<{ readonly agent: Schema.String; readonly bridge: Schema.brand<Schema.String, "BridgeId"> }>, Schema.Never, never, never>Since v0.1.0
Rpcs (class)
Section titled “Rpcs (class)”The protocol between a host and a bridge: the requests a host sends over its WebSocket, which
BridgeServer handles and BridgeClient sends.
Signature
declare class RpcsSince v0.1.0
Schemas
Section titled “Schemas”BridgeId
Section titled “BridgeId”Names a bridge after everything a host launches it with, as Sandbox.isName checks, so that a
host that would launch the same bridge finds the one running, and a host with other settings
launches its own.
Signature
declare const BridgeId: Schema.brand<Schema.String, "BridgeId">Since v0.1.0
BridgeId (type alias)
Section titled “BridgeId (type alias)”Signature
type BridgeId = typeof BridgeId.TypeSince v0.1.0
LaunchOptions
Section titled “LaunchOptions”What a host gives the bridge it launches, on the bridge’s standard input, so that neither the agent nor anything else in the sandbox can read the token or the agent’s credentials.
Signature
declare const LaunchOptions: Schema.Struct<{ readonly token: Schema.String readonly bridge: Schema.brand<Schema.String, "BridgeId"> readonly stateDirectory: Schema.String readonly agentOptions: Schema.Codec<Schema.Json, Schema.Json, never, never>}>Since v0.1.0
LaunchOptions (interface)
Section titled “LaunchOptions (interface)”Signature
export interface LaunchOptions extends Schema.Schema.Type<typeof LaunchOptions> {}Since v0.1.0
Listening
Section titled “Listening”What a bridge writes to listeningFile in its state directory once it listens, for hosts to find
it: the port. It holds no token, as the agent can read every file in the sandbox.
Signature
declare const Listening: Schema.Struct<{ readonly port: Schema.brand<Schema.Int, "Port"> }>Since v0.1.0
Listening (interface)
Section titled “Listening (interface)”Signature
export interface Listening extends Schema.Schema.Type<typeof Listening> {}Since v0.1.0
NumberedPart
Section titled “NumberedPart”A part of a turn, numbered from 1 in the order the turn streamed it.
Signature
declare const NumberedPart: Schema.Struct<{ readonly seq: Schema.Int readonly part: Schema.Union< readonly [ Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"text-start"> readonly id: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"text-delta"> readonly id: Schema.String readonly delta: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"text-end"> readonly id: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"reasoning-start"> readonly id: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"reasoning-delta"> readonly id: Schema.String readonly delta: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"reasoning-end"> readonly id: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"tool-params-start"> readonly id: Schema.String readonly name: Schema.String readonly providerExecuted: Schema.withDecodingDefaultKey<Schema.Boolean> readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"tool-params-delta"> readonly id: Schema.String readonly delta: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"tool-params-end"> readonly id: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.Struct<{ readonly type: Schema.tag<"tool-call"> readonly id: Schema.String readonly name: Schema.String readonly params: Schema.Unknown readonly providerExecuted: Schema.optional<Schema.Boolean> readonly metadata: Schema.optional< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>> > }>, Schema.Struct<{ readonly type: Schema.tag<"tool-result"> readonly id: Schema.String readonly name: Schema.String readonly result: Schema.Unknown readonly isFailure: Schema.Boolean readonly providerExecuted: Schema.optional<Schema.Boolean> readonly preliminary: Schema.optional<Schema.Boolean> readonly metadata: Schema.optional< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>> > }>, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"tool-approval-request"> readonly approvalId: Schema.String readonly toolCallId: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"file"> readonly mediaType: Schema.String readonly data: Schema.Uint8ArrayFromBase64 readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"source"> readonly sourceType: Schema.tag<"document"> readonly id: Schema.String readonly mediaType: Schema.String readonly title: Schema.String readonly fileName: Schema.optionalKey<Schema.String> readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"source"> readonly sourceType: Schema.tag<"url"> readonly id: Schema.String readonly url: Schema.URLFromString readonly title: Schema.String readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"response-metadata"> readonly id: Schema.optional<Schema.String> readonly modelId: Schema.optional<Schema.String> readonly timestamp: Schema.optional<Schema.DateTimeUtcFromString> readonly request: Schema.optional<typeof HttpRequestDetails> readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"finish"> readonly reason: Schema.Literals< ["stop", "length", "content-filter", "tool-calls", "error", "pause", "other", "unknown"] > readonly usage: typeof Usage readonly response: Schema.optional<typeof HttpResponseDetails> readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.toEncoded< Schema.Struct<{ readonly type: Schema.tag<"error"> readonly error: Schema.Unknown readonly "~effect/ai/Response/Part": Schema.withDecodingDefaultKey<Schema.tag<"~effect/ai/Response/Part">> readonly metadata: Schema.withDecodingDefault<Schema.$Record<Schema.String, Schema.Codec<Schema.Json>>> }> >, Schema.Struct<{ readonly type: Schema.tag<"finish-step"> readonly reason: Schema.Literals< ["stop", "length", "content-filter", "tool-calls", "error", "pause", "other", "unknown"] > readonly usage: Schema.toEncoded<typeof Usage> readonly metadata: Schema.optional< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>> > }>, Schema.Struct<{ readonly type: Schema.tag<"file-change"> readonly event: Schema.Literals<readonly ["create", "modify", "delete"]> readonly path: Schema.String readonly metadata: Schema.optional< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>> > }>, Schema.Struct<{ readonly type: Schema.tag<"compaction"> readonly trigger: Schema.Literals<readonly ["manual", "auto"]> readonly summary: Schema.String readonly kept: Schema.optional< Schema.$Array<Schema.toEncoded<Schema.Codec<Prompt.Message, Prompt.MessageEncoded, never, never>>> > readonly tokensBefore: Schema.optional<Schema.Int> readonly tokensAfter: Schema.optional<Schema.Int> readonly metadata: Schema.optional< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>> > }>, Schema.Struct<{ readonly type: Schema.tag<"turn-start"> readonly agentState: Schema.Codec<Schema.Json, Schema.Json, never, never> }> ] >}>Since v0.1.0
NumberedPart (interface)
Section titled “NumberedPart (interface)”Signature
export interface NumberedPart extends Schema.Schema.Type<typeof NumberedPart> {}Since v0.1.0
ResponseFormat
Section titled “ResponseFormat”How a turn answers: in text, or in JSON that matches schema, a JSON Schema.
Signature
declare const ResponseFormat: Schema.Union< readonly [ Schema.Struct<{ readonly type: Schema.tag<"text"> }>, Schema.Struct<{ readonly type: Schema.tag<"json"> readonly objectName: Schema.String readonly schema: Schema.Codec<Schema.Json, Schema.Json, never, never> }> ]>Since v0.1.0
ResponseFormat (type alias)
Section titled “ResponseFormat (type alias)”Signature
type ResponseFormat = typeof ResponseFormat.TypeSince v0.1.0
ToolDefinition
Section titled “ToolDefinition”A tool the host runs, as the agent reads it: its name, what it does, and its parameters.
Signature
declare const ToolDefinition: Schema.Struct<{ readonly name: Schema.String readonly description: Schema.optional<Schema.String> readonly parameters: Schema.Codec<Schema.Json, Schema.Json, never, never>}>Since v0.1.0
ToolDefinition (interface)
Section titled “ToolDefinition (interface)”Signature
export interface ToolDefinition extends Schema.Schema.Type<typeof ToolDefinition> {}Since v0.1.0
TurnId
Section titled “TurnId”Names a turn, as Sandbox.isName checks. The host picks it, so that a request to start the turn
that it sends again, not knowing whether the first arrived, starts it once.
Signature
declare const TurnId: Schema.brand<Schema.String, "TurnId">Since v0.1.0
TurnId (type alias)
Section titled “TurnId (type alias)”Signature
type TurnId = typeof TurnId.TypeSince v0.1.0
TurnOptions
Section titled “TurnOptions”A turn’s settings, as Agent.TurnOptions gives them, with the host’s tools and the answer’s
schema as JSON Schemas, since only data crosses into the sandbox.
Signature
declare const TurnOptions: Schema.Struct<{ readonly prompt: Schema.Struct<{ readonly role: Schema.Literal<"user"> readonly content: Schema.Union< readonly [ Schema.decodeTo< Schema.NonEmptyArray< Schema.toType< Schema.Struct<{ readonly type: Schema.Literal<"text"> readonly text: Schema.String readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey< Schema.Literal<"~effect/ai/Prompt/Part"> > readonly options: Schema.withDecodingDefault< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json>>> > }> > >, Schema.String, never, never >, Schema.$Array< Schema.Union< readonly [ Schema.Struct<{ readonly type: Schema.Literal<"text"> readonly text: Schema.String readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey< Schema.Literal<"~effect/ai/Prompt/Part"> > readonly options: Schema.withDecodingDefault< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json>>> > }>, Schema.Struct<{ readonly type: Schema.Literal<"file"> readonly mediaType: Schema.String readonly fileName: Schema.optional<Schema.String> readonly data: Schema.Union<readonly [Schema.String, Schema.Uint8Array, Schema.URL]> readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey< Schema.Literal<"~effect/ai/Prompt/Part"> > readonly options: Schema.withDecodingDefault< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json>>> > }> ] > > ] > readonly "~effect/ai/Prompt/Message": Schema.withDecodingDefaultKey<Schema.Literal<"~effect/ai/Prompt/Message">> readonly options: Schema.withDecodingDefault< Schema.$Record<Schema.String, Schema.NullOr<Schema.Codec<Schema.Json>>> > }> readonly instructions: Schema.optional<Schema.String> readonly model: Schema.optional<Schema.String> readonly tools: Schema.$Array< Schema.Struct<{ readonly name: Schema.String readonly description: Schema.optional<Schema.String> readonly parameters: Schema.Codec<Schema.Json, Schema.Json, never, never> }> > readonly responseFormat: Schema.Union< readonly [ Schema.Struct<{ readonly type: Schema.tag<"text"> }>, Schema.Struct<{ readonly type: Schema.tag<"json"> readonly objectName: Schema.String readonly schema: Schema.Codec<Schema.Json, Schema.Json, never, never> }> ] >}>Since v0.1.0
TurnOptions (interface)
Section titled “TurnOptions (interface)”Signature
export interface TurnOptions extends Schema.Schema.Type<typeof TurnOptions> {}Since v0.1.0