Skip to content

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

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
>

Source

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
>

Source

Since v0.1.0

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
>

Source

Since v0.1.0

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
}

Source

Since v0.1.0

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>
}

Source

Since v0.1.0

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
}

Source

Since v0.1.0