Hosted A2A and ACP agents
What reef is, what it bridges, and what is in (and out of) the v1 connector.
docs/agents/60-reef.mdxOn this page
@octostaff/reef is the connector worker that bridges
Bubble and hosted A2A
agents. Bubble is a WebSocket server and bot adapters like
claude-scuba are clients that dial in. Some
useful agents are the inverse — long-lived HTTP services that expect
to be called, not to dial out. reef is the dedicated process whose
only job is to own the call-side connection to one hosted agent and
relay its conversation with Bubble.
When to Use It
Use @octostaff/reef when your agent should be driven by a worker process
rather than dialing into Bubble itself — a hosted A2A agent behind an HTTP
endpoint, or an ACP-compatible agent reef launches as a subprocess. Reef is a
good fit for server-side agents, containerized agents, and scale-to-zero
deployments where the agent cannot keep its own long-lived Bubble WebSocket
connection open.
Use @octostaff/claude-scuba instead when a local Claude Code process should
dial into Bubble directly.
What the package does
reef runs one connector per process, attached to one A2A agent. On startup it:
- Authenticates to a Bubble server as a configured
botprincipal and confirms the identity via/v1/auth/whoami. - Resolves the agent's
AgentCard at
/.well-known/agent-card.jsonto pick a streaming binding (HTTP+JSONor canonicalJSONRPC). - Subscribes to the principal's grants stream and opens one thread runtime per joined thread. New thread grants start runtimes live; revocations stop them.
- Applies the configured bounded
backlogpolicy from a local settlement checkpoint, then forwards every completed user run (a foreign principal'srun_started … run_finished) to the agent as a single A2Amessage:streamcall. - Translates the agent's SSE replies — text deltas, structured
dataparts, tool calls / tool responses, terminal task states — back into Bubblemessage_partchunks framed byrun_started/run_finished, and appends them to the thread. - Reacts to mid-stream
run_interrupt_requestedand cross-participanttool_call_resultevents: an interrupt aborts the in-flight fetch and issues an A2Atasks/cancel; a tool result is forwarded to the agent as a function-response message that continues the same A2A task.
v1 scope
In:
- One bot principal per process, one hosted A2A agent.
- Auto-detected transport: HTTP+JSON streaming and canonical JSON-RPC
message/stream. - Per-thread runtime driven by the live grants subscription; bounded replay from a local settlement checkpoint followed by an ordered live tail.
- Bubble user run → A2A user message; agent text artifacts → Bubble
text deltas; function-call / function-response
dataparts → Bubbletool-input-available/tool-output-availablechunks; unrecognizeddataparts →agent_eventnative parts. - Snapshot deduplication for agents (notably Google ADK /
docker agent serve a2a) that re-send the entire reply as a final part. - Cooperative interrupt:
run_interrupt_requestedaborts the in-flight forward and issuestasks/cancelon the JSON-RPC binding. - Bearer-token auth to both Bubble (local bot token) and the A2A
agent (optional
Authorization: Bearer).
Out (deferred):
- Multi-agent per process.
- Persistent always-on mode and lease-based ownership for scale-to-zero triggered runs (the connector is currently a long-lived process).
- Pushing user input into a sub-task that the agent spawned itself.
- Cost / usage telemetry surfaced into Bubble.
Where to go next
- How reef Works — the relay's state machine, the drain rule, and how interrupts unwind a streaming fetch cleanly.
- Launching a Connector — config schema,
the
reefCLI, the bundled mock A2A agent, and embedding the connector inside a larger Node process.