> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mounting Agents

> Expose every agent your function serves with one call: mountAgents gives WebSocket, SSE, and RPC one address per agent, an edge authorization gate that composes with the agent's own, and names your customers already use.

An agent is only reachable through a function. Before `mountAgents`, that meant
hand-writing the front door: parse the URL, check for an upgrade, pull the credential,
encode the name, pick the binding, call `idFromName(...).fetch(req)`, and answer health
checks — the same file in every project, subtly different each time.

`mountAgents` *(SDK ≥ 0.12.0)* is that file, already written. You supply the routing
table and your policy; it supplies everything else.

```ts theme={null}
import { mountAgents } from "@telnyx/edge-runtime/mount";
import type { ActorNamespace } from "@telnyx/edge-runtime";

interface Env {
  CONVERSATION: ActorNamespace;
  BILLING: ActorNamespace;
}

export default {
  fetch: mountAgents<Env>((env) => ({
    conversation: env.CONVERSATION,
    billing: env.BILLING,
  })),
};
```

<Warning>
  **`fetch:` is not optional.** `mountAgents` returns a handler, and a function entry
  must default-export an **object** carrying that handler on `fetch`. Writing
  `export default mountAgents(map)` produces a bundle the runtime refuses at load time —
  the deploy does not start, so there is no request to debug.
</Warning>

The map is the entire routing table. Nothing is discovered, and no name is derived from
a class name: an agent is reachable once it is written there, and an unknown mount key
is a `404` with no binding consulted. It is a function of `env`, so bindings resolve per
request and nothing is cached between them.

## One address, three transports

Each mounted agent answers on `<base>/<mount>/<name>`:

* `<base>` is the path prefix the mount owns — `/agents` unless you set
  [`base`](/docs/agent-sdk/api-reference/mount#mountoptions).
* `<mount>` is a key of the map above.
* `<name>` is the routing name of one instance — one agent, one persistent identity.

So the function above serves `/agents/conversation/alice` and
`/agents/billing/acct-1004`.

**The shape of the request selects the transport, not the path.** The same address
answers all three:

| The client sends     | Address                                      | It gets                   |
| -------------------- | -------------------------------------------- | ------------------------- |
| `Upgrade: websocket` | `/agents/conversation/alice`                 | the agent socket protocol |
| `GET`                | `/agents/conversation/alice?subscribe=state` | Server-Sent Events        |
| `POST`               | `/agents/conversation/alice/rpc/humanReply`  | one RPC call              |

This is why the [AgentClient](/docs/agent-sdk/api-reference/agent-client) reference
connects to `wss://my-func.telnyxcompute.com/agents/conversation/alice` — that URL is
the default `base`, the `conversation` mount key, and the name `alice`. A browser
talking to the function above needs no route of its own:

```ts theme={null}
import { AgentClient } from "@telnyx/edge-runtime/client";

const agent = new AgentClient<DeskStub, DeskState>(
  "wss://my-func.telnyxcompute.com/agents/conversation/alice",
  { token: sessionToken, subscribe: ["state", "messages"], resume: true },
);
agent.onState(render);
await agent.stub.humanReply("On it.");
```

An RPC call from a page with no SDK at all is the same address with a method on the
end:

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '["On it."]' \
  https://my-func.telnyxcompute.com/agents/conversation/alice/rpc/humanReply
```

The request body is the argument list, and the response is the same `result` / `error`
frame a WebSocket caller would receive. Only methods decorated with
[`@rpc()`](/docs/agent-sdk/api-reference/agent/rpc) are callable.

<Warning>
  **SSE does not stream on deployed functions today.** The edge gateway buffers a
  response until it completes before returning it, so an open-ended `text/event-stream`
  never reaches the client — an `EventSource` against a deployed mount connects and then
  receives nothing. The WebSocket and RPC paths are unaffected, and SSE works normally in
  local development.

  Use the WebSocket path in production. It carries the same frames under the same policy
  on the same address, so the change is `new WebSocket(url)` in place of
  `new EventSource(url)` — or `AgentClient`, which does the reconnect and resume for you.
  This is a platform-side gap tracked as **COMPUTE-819**; the SSE surface stays and starts
  streaming once the gateway allows it. The full caveat is on the
  [AgentHttpServer](/docs/agent-sdk/api-reference/agent-http-server) page.
</Warning>

## Authorization composes — it does not replace

The mount adds **no policy of its own**. It routes, and it runs the one `authorize`
callback you give it. That callback does not stand in for the agent's own
`authorize(token, req)`: both run, and they do different jobs.

**Gate one, at the edge.** `authorize(req, route)` runs after the address resolves and
**before anything touches an actor**, on every transport — an upgrade, an SSE stream,
and each RPC POST alike. It receives the request unmodified and the resolved
[route](/docs/agent-sdk/api-reference/mount#mountroute): the mount key, the decoded
name, the actor id, the transport, and the method for an RPC. Answer with a `Response`
to reject (nothing is forwarded, no actor wakes), with headers to admit and stamp, or
with nothing to admit as-is.

**Gate two, inside the agent.** The caller's credential rides through **untouched** —
`?token=` or an `Authorization` header, exactly as sent. The agent's
[`authorize`](/docs/agent-sdk/api-reference/agent/connection#authorize) resolves this
connection's [claims](/docs/agent-sdk/api-reference/agent/connection#claim) from it
exactly as it would with no mount in front, and
[`onAttach`](/docs/agent-sdk/api-reference/agent/connection#onattach) may still veto.

```ts theme={null}
// The function: who is this, and may they address this agent at all?
export default {
  fetch: mountAgents<Env>((env) => ({ conversation: env.CONVERSATION }), {
    authorize: async (req, { name }) => {
      const user = await verify(req.headers.get("authorization"));
      if (!user) return new Response("unauthorized", { status: 401 });
      if (user.deskId !== name) return new Response("forbidden", { status: 403 });
      return { "x-desk-user": user.id, "x-desk-role": user.role };
    },
  }),
};
```

```ts theme={null}
// The agent: given the credential, what may this connection DO?
import { Agent, type Claim } from "@telnyx/edge-runtime";

export class Conversation extends Agent<Env, DeskState> {
  protected override authorize(token: string | undefined): readonly Claim[] {
    if (token === undefined) return ["read"];        // anonymous watcher
    return isOperator(token) ? ["read", "rpc"] : ["read"];
  }
}
```

Identity at the mount, grants in the agent. Two rules make the stamped identity safe to
trust:

* **A stamped header overwrites** any header of the same name the caller sent, so a
  client cannot forge an identity your agent trusts.
* **Headers you do not stamp are forwarded as the caller sent them.** Trust only what
  you stamp.

A throw from the mount's `authorize` is not an admission — it propagates and the request
never reaches an actor. Reading the body is safe: the agent is handed an independent
copy, so a signature you verify here still arrives there intact. That copy is made only
for a request that carries a body, and it does buffer while both copies are outstanding
— on a large streamed upload, prefer deciding from the headers.

## Names your customers already use

Actor ids are not free-form: only ASCII letters, digits, `-`, `_`, and `.` are
addressable, and an id built from anything else belongs to an instance that never
activates. The names you actually route on are not so confined — a phone number, an
email address, a composite key, a name with accents or emoji.

The mount decodes the name segment and re-encodes it with
[`encodeAgentName`](/docs/agent-sdk/api-reference/mount#encodeagentname), which maps any
name onto the addressable set reversibly:

```ts theme={null}
import { encodeAgentName, decodeAgentName } from "@telnyx/edge-runtime/mount";

encodeAgentName("alice");       // "alice"           — already addressable, unchanged
encodeAgentName("+15550100");   // ":2b:15550100"
encodeAgentName("user@ex.com"); // "user:40:ex.com"

decodeAgentName(":2b:15550100"); // "+15550100"      — render an id back for a human
```

`/agents/conversation/%2B15550100` therefore addresses the agent for `+15550100`, and
no two names ever share an instance. Three things to keep in mind:

* **The mapping is part of an actor's identity and is frozen.** The id a name first
  resolves to is where that instance's state and timers live. Do not re-derive the rule
  by hand — call `encodeAgentName` from any front door of your own that addresses the
  same instances, or it will point the name at a different, empty instance.
* **Case is significant.** `alice` and `Alice` are two agents. Lowercase names before
  routing on them if you mean them to be one.
* **Encode the raw name exactly once.** Encoding an already-encoded name escapes its
  `:` signs and addresses something else.

### How long a name may be

The limit is on the actor id **once URL-escaped** — `MAX_ESCAPED_ACTOR_ID_LENGTH`,
**225** characters — because the id is carried into the name of the file the instance's
persistent state is kept in, which is capped at 255 bytes. How many characters of *name*
that allows depends on which characters they are, since an escape is longer than what it
replaces:

| A name of…                                  | costs per character | one run fits |
| ------------------------------------------- | ------------------- | ------------ |
| ASCII letters, digits, `-`, `_`, `.`        | 1                   | 225          |
| accented Latin, Greek, Cyrillic (2 bytes)   | 4                   | 54           |
| most CJK and symbols such as `☕` (3 bytes)  | 6                   | 36           |
| emoji and other astral characters (4 bytes) | 8                   | 27           |

Every separate escaped run also spends 6 on the pair of `:` bracketing it, so a mixed
name fits less than any single row suggests. A name whose id would exceed the ceiling is
refused by the mount with `400 malformed_name` before any actor wakes, and
`encodeAgentName` throws rather than handing back an unusable id.

<Warning>
  **An over-long id does not fail on first use.** The instance activates, serves requests,
  and accepts writes; the long name is only needed when it is loaded again after being
  evicted from memory, and from then on the instance cannot be opened and what it stored
  is unreachable. Nothing about the first, healthy run of it looks wrong — which is why
  the limit is checked up front. See
  [Limits](/docs/agent-sdk/limits#max_escaped_actor_id_length) and
  [`escapedActorIdLength()`](/docs/agent-sdk/limits#escapedactoridlength).
</Warning>

## What the mount answers itself

| Request                                                                               | Answer                                                                        |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `/health/liveness`, `/health/readiness`                                               | `200 ok`, no actor woken, nothing about what is mounted in the body           |
| A path outside `base`                                                                 | your [`fallback`](/docs/agent-sdk/api-reference/mount#mountoptions), or `404` |
| Unknown mount key, malformed address, empty name                                      | `404` — one body for all of them                                              |
| A name with no faithful actor id                                                      | `400`, naming the problem                                                     |
| A shape matching no transport — a `PUT`, a `GET` on an RPC address, an upgrade to one | `405` with an `Allow` header                                                  |

No actor is woken for any of them. Refusals from the mount carry a JSON body with an
`error` of `not_found`, `method_not_allowed`, or `malformed_name`; answers from the
agent — including its own errors — are relayed unchanged. Set `health: false` when the
surrounding function owns its own health routes.

## Composing with routes of your own

A mount does not have to own the whole function. `fallback` receives every request whose
path is not under `base`, so your own handler keeps its routes and the agent addresses
never reach it:

```ts theme={null}
export default {
  fetch: mountAgents<Env>((env) => ({ conversation: env.CONVERSATION }), {
    base: "/agents",          // the paths the mount owns
    fallback: myRouter.fetch, // everything else stays yours
    health: false,            // ...including your own health checks
  }),
};
```

## Cross-origin browsers

Absent `cors`, the mount sends no CORS headers and answers `OPTIONS` with `405`, so a
page from another origin cannot reach it. Opt in when a browser page served elsewhere
must open an `EventSource` or `fetch` an RPC address:

```ts theme={null}
{
  cors: {
    origin: ["https://desk.example.com"],
    credentials: true, // requires a concrete origin — never "*"
  },
}
```

It is a passthrough and nothing more: it does not authenticate, and it never substitutes
for `authorize`. Mount names are ordinary path segments, not secrets — they are part of
the address every legitimate client already knows.

## Next

* [`mountAgents` reference](/docs/agent-sdk/api-reference/mount) — every option, type,
  and refusal in full
* [AgentClient](/docs/agent-sdk/api-reference/agent-client) — the browser/Node client
  that connects to the address above
* [AgentSocketServer](/docs/agent-sdk/api-reference/agent-socket-server) and
  [AgentHttpServer](/docs/agent-sdk/api-reference/agent-http-server) — what answers on
  the agent side of each transport
* [WebSockets](/docs/agent-sdk/websockets) — the platform surface underneath, and what
  a hand-written front door looks like
