Managoat is the hosted Fountain. Fountain is the open-source engine, and its name is on the CLI, the API, the SDK and this manual. Everything here applies to Managoat unless a page says it is for a self-hosted server.

OpenBot (AG-UI)

OpenBot is CopilotKit's self-hosted agent platform. It has channels, a coworker roster, an audit trail, and a browser computer for each bot. A "Bot" there is not a framework and not an SDK. It is any HTTP endpoint that speaks AG-UI, the open protocol for interaction between an agent and a user.

Fountain speaks it. POST /api/agui/:agent_id takes AG-UI's RunAgentInput and answers with its SSE event stream. A Fountain agent therefore arrives in OpenBot as a coworker with a channel of its own. There is no plugin, no sidecar, and no code on the OpenBot host.

OpenBot (channels, roster, audit) ──HTTPS──▶ Fountain ──▶ sandbox: the Fountain agent
one channel = one thread POST /api/agui/:id (its environment, vault, runtime)
◀── SSE: RUN_STARTED, TEXT_MESSAGE_*, THINKING_*, RUN_FINISHED

The endpoint is not specific to OpenBot. Any AG-UI host registers a Fountain agent the same way, whether it is a hand-written client or another product built on the protocol. OpenBot is the host we built it against and verified it against, and nothing more.

At a glance

Direction Inbound. OpenBot drives Fountain.
Talks over AG-UI, at POST /api/agui/:agent_id.
Configured on The OpenBot host.
Plugin None. A URL is the whole integration.
Credential An API key that the coworker holds.
Scope One channel is one conversation is one sandbox.

Set it up

You need two things from Fountain. The agent's id, and an API key.

fountain agent list # the id
fountain keys create openbot

In OpenBot, open /agents, create a coworker, then complete four fields.

Field Value
Name / Title Whatever you want the coworker called.
Role description The role it always has, as below.
AG-UI endpoint https://your-fountain/api/agui/<agent_id>
Authorization header Authorization / Bearer ftn_...

OpenBot's Test connection button runs a real AG-UI run against the endpoint before it saves. So the form tells you about a typo, a dead host or a wrong key, and a channel does not. That test provisions a sandbox to answer, and that sandbox counts against your concurrency quota until it idles out.

If you run both on one laptop, OpenBot refuses a loopback endpoint. Set AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true in its .env. It is the same target check that governs browser navigation, and OpenBot ships it set for local development.

You can declare the coworker in a tenant package instead.

agents:
- id: ada
name: Ada
title: Fountain Agent
role_description: Runs in its own sandbox.
type: remote-ag-ui
endpoint: https://your-fountain/api/agui/<agent_id>

One channel is one conversation is one sandbox

Understand this part. The two products disagree about where an agent's memory lives, and the endpoint resolves that in Fountain's favour.

An AG-UI host holds the transcript. It replays the whole message list on each run, and expects the endpoint to be stateless.

A Fountain conversation is the opposite. The sandbox holds the context, which is the agent's files, its shell history, and what it worked out last turn. Replay the transcript into it each turn, and you feed the agent its own words back.

So Fountain maps the thread, and does not replay it. OpenBot mints a stable thread id for each channel. Fountain binds the channel as agui:<threadId>, then sends the newest user message of each run as the prompt. The first run on a thread opens a conversation. Each later run prompts the conversation already bound to it.

The results are the ones you would want.

  • A new channel in OpenBot starts a new sandbox.
  • Two channels with the same coworker are two sandboxes, and neither knows about the other.
  • A channel continues where it stopped. That holds after the sandbox idled out and suspended, and the next message wakes it.
  • Fountain scopes that link to one tenant. Two accounts whose hosts happen to mint the same thread id share nothing.

The conversations appear in Fountain like any others, with a channel_id of agui:<threadId>. The API filters on it. Read Conversations.

curl -H "Authorization: Bearer ftn_..." \
"https://your-fountain/api/conversations?channel_id=agui:<threadId>"

The standing role

OpenBot sends a coworker's title and role description as an AG-UI system message on each run. Fountain delivers it once, with the first prompt of a new conversation. After that the agent has it, and to repeat it each turn is noise in the transcript and tokens on the bill.

That choice has a cost. Edit a role in OpenBot and it reaches a new channel. It does not reach a sandbox that already booted with the old one. Start a new channel to apply a role you rewrote.

An agent's own system prompt still applies. It is the better place for whatever must hold on each surface.

What comes back

Fountain AG-UI
text blocks TEXT_MESSAGE_START / CONTENT / END
thinking blocks THINKING_*
tool_use and tool_result THINKING_*, one line for each call, such as → Terminal and ← ok.
The provision, setup and other stages THINKING_*, such as provision: started.
turn/done RUN_FINISHED
turn/failed RUN_ERROR, which carries the reason.

?activity=off on the endpoint drops everything except the reply text.

Fountain never emits an AG-UI tool call, and that is deliberate. On this protocol a tool call means host, run this and send me the result. The run ends, the host executes it, and a second run carries the result back.

A Fountain agent ran its own tool, in its own sandbox, and it already has the result. To report that as a call would ask the host to execute something twice. It would also leave the run to wait for a result that never comes. So Fountain reports tool activity as reasoning, which no host tries to execute.

Here is the other side of that coin. OpenBot's governance does not reach inside a Fountain sandbox. Its boundary is the gateway that each browser, file and MCP tool call passes through. A Fountain agent's bash never goes near it, so /admin/audit and /admin/boundaries have nothing to say about what the agent did.

Fountain governs a Fountain agent, with its own audit trail, its environment, its vault and its sandbox provider. Register one as a coworker in the knowledge that you trust Fountain's boundary, and not OpenBot's.

The other half of this integration would bridge OpenBot's tools into the sandbox. Its computer, its components and its MCP grants would become things a Fountain agent can call and OpenBot can refuse. Nobody built that.

Timing, and a watchdog to know about

OpenBot ends a turn whose stream has been silent for AGENT_STALL_TIMEOUT_MS, which is a minute by default. It measures silence, and not duration. A turn can run for an hour, as long as more events arrive.

To provision a fresh sandbox takes longer than a minute on some providers, and a first run would otherwise die mid-provision. Two things keep it alive. The lifecycle stages stream as thinking events, and the endpoint writes an SSE heartbeat comment every 15 seconds. Both are bytes on the wire, which is what the watchdog counts.

Whether a host renders a thinking event is its own business. OpenBot's channel view, on CopilotKit 1.67.1, does not show them. So a first run looks quiet even while the connection is healthy. A later run on a warm sandbox answers in a couple of seconds.

If a Fountain error arrives before the stream opens, it is an ordinary JSON response with a status, and OpenBot shows it in the channel. An unknown agent, no credit and the sandbox quota are the three. Once the stream is open, a failure arrives as RUN_ERROR.

Model credentials

The agent needs a model credential here, as it does on each other surface. Add one at /account/inference-credentials. That is an Anthropic API key or a Claude Code OAuth token, an OpenAI key, or a Gemini key.

Without one the sandbox spawns, the ACP session initialises, and the turn fails with Authentication required. That reaches the channel as a RUN_ERROR that names it.

Fountain has no platform-level model key, on purpose (ADR 0008).

What it does not do yet

  • No tool bridge, as above.
  • One key, one tenant. The bot's stored key is a Fountain API key, so each OpenBot user in a deployment reaches Fountain as the account that owns it. OpenBot's own actor.id does not travel.
  • No approvals. OpenBot can hand control to a person mid-run. A Fountain agent that asks permission for a tool call has nowhere to ask on this protocol. The API answers such a request at POST /api/conversations/:id/requests/:request_id, and the AG-UI endpoint does not carry it to the host (#643).
  • Attachments do not travel. An attachment on an OpenBot message does not reach Fountain. The prompt is text.

Verify it

Against a live instance, with curl:

curl -N -X POST "https://your-fountain/api/agui/<agent_id>" \
-H "Authorization: Bearer ftn_..." \
-H "content-type: application/json" \
-H "accept: text/event-stream" \
-d '{"threadId":"probe-1","runId":"run-1",
"messages":[{"role":"user","content":"Say hello in five words."}],
"tools":[],"context":[],"state":{},"forwardedProps":{}}'

A healthy run is RUN_STARTED, then some TEXT_MESSAGE_*, then RUN_FINISHED. Each SSE message is data: {"type": ...}, so the type sits inside the JSON. That is what the reference AG-UI encoder writes, and what each client parses.