BridgeServer
The bridge’s side of Bridge.Rpcs, which runs in the sandbox and serves a driver’s sessions to
hosts. serve serves them over a WebSocket at /, refusing a host that does not present the
bridge’s token, and layerHandlers is the same bridge without a transport. readLaunchOptions
reads what the host that launched the bridge gave it.
Once it listens, the bridge writes the port to Bridge.listeningFile in its state directory,
where hosts find it. It exits once it runs no turn and either no host has started a session or a
turn there for its idle timeout, or the file names another port: another bridge of its id took
its place, as when two hosts launched one at once.
Each session the bridge starts runs in a scope of its own until a host stops or destroys it. The bridge runs the session’s turns itself, one at a time, in that scope, so that a turn does not end with the connection of the host that started it; a host that starts another turn has moved on from the one running, as when the host that started it is gone, so the bridge aborts it rather than refuse the new turn. It numbers a turn’s parts as the turn streams them and keeps them in memory and in a file in its state directory, a line each, followed by how the turn ended. A session keeps only its latest turn, which hosts read from any number while it runs and after it ends. A bridge started again reads a turn from its file. A turn whose file has no end, which the bridge ran when it died, ended then, aborted. A turn the bridge aborted that a host starts again continues, in the conversation the agent kept, from the steps it finished.
Since v0.1.0
Constructors
Section titled “Constructors”readLaunchOptions
Section titled “readLaunchOptions”What the host that launched the bridge gave it on standard input, as one line of JSON. The bridge reads no further, since a sandbox may never end a process’s standard input, as Daytona does not.
Signature
declare const readLaunchOptions: Effect.Effect< { readonly token: string readonly bridge: string & Brand<"BridgeId"> readonly stateDirectory: string readonly agentOptions: Schema.Json }, PlatformError | Schema.SchemaError, Stdio.Stdio>Since v0.1.0
Serves driver’s sessions over a WebSocket at /, on the HttpServer provided, and writes the
server’s port to Bridge.listeningFile, until the bridge has been idle for its idle timeout or
another bridge of its id takes its place.
Details
The sessions end, aborting their turns, when the scope closes.
Example (A bridge program on Bun)
import { Bridge, BridgeServer } from "@amends-dev/harness"import { BunHttpServer, BunRuntime, BunServices } from "@effect/platform-bun"import { Effect, Layer, Redacted, Schema } from "effect"
// The adapter's driver, made from the agent options the host launched the bridge with.declare const makeDriver: (agentOptions: Schema.Json) => Effect.Effect<Bridge.Driver>
const program = Effect.gen(function* () { const launch = yield* BridgeServer.readLaunchOptions const driver = yield* makeDriver(launch.agentOptions) yield* BridgeServer.serve(driver, { token: Redacted.make(launch.token), bridge: launch.bridge, stateDirectory: launch.stateDirectory })})
// The sandbox picks the port; the bridge writes it where hosts look for it.BunRuntime.runMain( program.pipe( Effect.scoped, Effect.provide(Layer.merge(BunHttpServer.layer({ hostname: "0.0.0.0", port: 0 }), BunServices.layer)) ))Signature
declare const serve: ( driver: Bridge.Driver, options: Options) => Effect.Effect< undefined, PlatformError | Schema.SchemaError, HttpServer.HttpServer | Scope.Scope | Path.Path | FileSystem.FileSystem>Since v0.1.0
Layers
Section titled “Layers”layerHandlers
Section titled “layerHandlers”Handles Bridge.Rpcs for driver’s sessions, over whichever transport serves them, recording in
activity the turns they run and when a host was last active.
Details
serve provides it to an RpcServer over a WebSocket; a test, or a bridge with a transport of
its own, provides it to another.
Signature
declare const layerHandlers: ( driver: Bridge.Driver, options: HandlerOptions) => Layer.Layer< | Handler<"Hello"> | Handler<"StartSession"> | Handler<"StartTurn"> | Handler<"ReadTurn"> | Handler<"AbortTurn"> | Handler<"SubmitToolResult"> | Handler<"Approve"> | Handler<"Compact"> | Handler<"ReadState"> | Handler<"StopSession"> | Handler<"DestroySession">, never, Path.Path | FileSystem.FileSystem>Since v0.1.0
Models
Section titled “Models”Activity (interface)
Section titled “Activity (interface)”How busy a bridge is: how many turns it runs, and when a host last started a session or a turn there, or a turn last ended, in milliseconds since the epoch.
Signature
export interface Activity { readonly turns: number readonly lastActiveAt: number}Since v0.1.0
Options
Section titled “Options”HandlerOptions (interface)
Section titled “HandlerOptions (interface)”What the handlers take of serve’s options, with the activity they record.
Signature
export interface HandlerOptions extends Pick<Options, "stateDirectory" | "bridge"> { /** Where the handlers record the turns they run and when a host was last active. */ readonly activity?: Ref.Ref<Activity>}Since v0.1.0
Options (interface)
Section titled “Options (interface)”What serve serves a driver with: the bridge’s token, its id, its state directory, as the
host’s Bridge.LaunchOptions give them, and how long it waits idle before it exits.
Signature
export interface Options { /** The token hosts present when they connect. */ readonly token: Redacted.Redacted<string>
/** The bridge's id, which `Hello` answers. */ readonly bridge: Bridge.BridgeId
/** * The absolute directory, outside every session's directory, that the bridge keeps its turns in * and writes `Bridge.listeningFile` to. */ readonly stateDirectory: string
/** * How long the bridge waits for a host while it runs no turn, before it exits; 10 minutes by * default. */ readonly idleTimeout?: Duration.Input}Since v0.1.0