> ## 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.

# mountAgents

> The front door: one call routes every agent your function serves — WebSocket, SSE, and RPC on one address — plus the mount options, the name encoder, and the routing types.

`mountAgents` *(SDK ≥ 0.12.0)* is the front door for a function that serves agents.
One call replaces the routing file you would otherwise hand-write — URL parsing,
upgrade detection, credential extraction, name encoding, the hand-off to a namespace,
health routes — and leaves your function holding only the policy that is actually
yours. It ships at `@telnyx/edge-runtime/mount`.

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

interface Env {
  CONVERSATION: ActorNamespace;
}

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

<Warning>
  **The entry must export an object carrying `fetch`, not the handler itself.**
  `mountAgents` returns a `fetch` handler, so it goes on the `fetch` property of the
  module's default export. A bare-function default export —
  `export default mountAgents(map)` — is refused by the function runtime at load time,
  and the failure is a bundle that never starts rather than a request that 500s.
</Warning>

## The address, and what selects the transport

Every agent you mount answers on the same shape, `<base>/<mount>/<name>`, where
`<mount>` is a key of your map and `<name>` is the routing name of one instance. The
**request** picks the transport; the path never changes:

| Request              | Address                                 | Serves                    |
| -------------------- | --------------------------------------- | ------------------------- |
| `Upgrade: websocket` | `<base>/<mount>/<name>`                 | the agent socket protocol |
| `GET`                | `<base>/<mount>/<name>?subscribe=state` | Server-Sent Events        |
| `POST`               | `<base>/<mount>/<name>/rpc/<method>`    | one RPC call              |

`base` defaults to `/agents`, so the map above serves
`/agents/conversation/alice` on all three. **That is the URL the
[AgentClient](/docs/agent-sdk/api-reference/agent-client) examples connect to** —
`wss://my-func.telnyxcompute.com/agents/conversation/alice` is this default `base`,
this mount key, and this name:

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

// mount key ─────────────────────────┐    ┌─── routing name
const agent = new AgentClient(
  "wss://my-func.telnyxcompute.com/agents/conversation/alice",
);
```

A hand-written front door can serve any shape it likes; `mountAgents` is what makes
*this* shape route. The upgrade is handed to the agent's `webSocket`; `GET` and `POST`
are handed to the agent's `fetch`, where an
[AgentHttpServer](/docs/agent-sdk/api-reference/agent-http-server) over the agent's
connection engine answers them. The forwarded request keeps its URL and query string,
so `subscribe`, `Last-Event-ID` resume, and the `/rpc/<method>` segment all arrive as
the caller wrote them.

<Warning>
  **SSE does not stream on deployed functions today.** The edge gateway buffers a
  response until it completes, 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. This is a platform-side gap tracked as **COMPUTE-819**; see
  [AgentHttpServer](/docs/agent-sdk/api-reference/agent-http-server) for the full caveat.
  Use the WebSocket path in production — same address, same policy.
</Warning>

## Two gates, not one

The mount's [`authorize`](#mountoptions) does not replace the agent's own
`authorize(token, req)`. It **composes with** it, and both run:

1. **At the edge, before any actor wakes** — `authorize(req, route)` sees the inbound
   request and what the mount resolved from it (`mount`, `name`, `actorId`,
   `transport`, and for RPC the `method`). Return a `Response` to reject — nothing is
   forwarded and no actor is woken — or return headers to stamp identity onto the
   request, or nothing to admit as-is. It runs identically on all three transports, so
   no request shape reaches an agent around it.
2. **Inside the agent** — the caller's credential rides through untouched (`?token=`,
   or an `Authorization` header), so
   [`Agent.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 unmounted, and `onAttach` may still veto.

Stamp identity at the mount; decide grants in the agent.

```ts theme={null}
export default {
  fetch: mountAgents<Env>((env) => ({ conversation: env.CONVERSATION }), {
    authorize: async (req, { mount, name }) => {
      const user = await verify(req.headers.get("authorization"));
      if (!user) return new Response("unauthorized", { status: 401 });
      if (user.id !== name) return new Response("forbidden", { status: 403 });
      return { "x-desk-user": user.id }; // stamped — overwrites any caller header
    },
  }),
};
```

A stamped header always wins: it overwrites a header of the same name the caller sent,
so a client cannot forge an identity the agent trusts. Headers you do **not** stamp are
forwarded as the caller sent them — trust only what you stamp.

The [Mounting Agents](/docs/agent-sdk/mounting-agents) guide walks the whole path,
including composing the mount with routes of your own and the cross-origin opt-in.

## mountAgents()

> **mountAgents**\<`E`>(`map`, `options?`): (`req`, `env`) => `Promise`\<`Response`>

Route every agent in one call: `mountAgents` returns the `fetch` handler an
umbrella function exports, so the front door you would otherwise hand-write
— URL parsing, upgrade checks, credential extraction, name encoding, the
hand-off, health routes — is already written, and the only thing left in
your function is the policy that is actually yours.

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

export default {
  fetch: mountAgents((env: Env) => ({ support: env.SUPPORT }), {
    authorize: async (req, { name }) => {
      const user = await verify(req);
      if (!user) return new Response("unauthorized", { status: 401 });
      return { "x-desk-user": user.id };
    },
  }),
};
```

`mountAgents` returns the handler, not the module's default export: a
function entry must export an OBJECT carrying a `fetch` handler, so the
result goes on the `fetch` property. Exporting the returned function
directly is refused at load time.

**One address, every transport**

Callers get one shape, identical across every agent you mount, and the
**request** selects the transport — the path never changes:

| Request              | Address                                 | Serves                    |
| -------------------- | --------------------------------------- | ------------------------- |
| `Upgrade: websocket` | `<base>/<mount>/<name>`                 | the agent socket protocol |
| `GET`                | `<base>/<mount>/<name>?subscribe=state` | Server-Sent Events        |
| `POST`               | `<base>/<mount>/<name>/rpc/<method>`    | one RPC call              |

`<base>` defaults to `/agents`, so a `support` mount serves
`/agents/support/alice` on all three. GET and POST are handed to the agent's
own `fetch`, where an `AgentHttpServer` over the agent's connection engine
answers them; the upgrade is handed to the agent's `webSocket`. The
forwarded request keeps its URL and query string, so the SSE `subscribe`
filter, `Last-Event-ID` resume, and the `/rpc/<method>` segment all arrive
exactly as the caller wrote them.

**Names**

The name segment is decoded and then re-encoded to an actor id with
[`encodeAgentName`](/docs/agent-sdk/api-reference/mount#encodeagentname), so names that are natural for
customers — `+15550100`, an email address, a composite key, a name with
accents or emoji — address a persistent instance without the caller thinking
about encoding.

An actor id is **not** free-form: only ASCII letters, digits, `-`, `_`, and
`.` are addressable, and an id built from anything else belongs to an
instance that never activates. `encodeAgentName` is what stands between a
natural name and that constraint — it maps every other character onto the
addressable set reversibly, so no two names share an id. Names that are
already addressable pass through byte-for-byte, so mounting an existing
agent does not re-point it. A hand-written front door addressing the same
instances should call `encodeAgentName` for the identical result.

**That mapping is part of an actor's identity and does not change.** The id
a name first resolves to is where that instance's state and timers live;
re-deriving the rule by hand, or expecting it to change, re-points a name at
a different and empty instance. Letter case is significant throughout —
`alice` and `Alice` are two instances.

A name that has no faithful id — one that is empty, one whose escaped id
would be longer than [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) characters, or
malformed Unicode — is refused with a `400` naming the problem, on the
request that carried it. Nothing is addressed and no actor is woken. How long
a name may be depends on the characters in it, since escaping expands them;
see [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length).

**The map is the whole routing table**

Only what your map returns is reachable. There is no discovery and no
derivation from class names: an unknown mount key is a `404` and no binding
is consulted for it.

**What it answers itself**

* `/health/liveness` and `/health/readiness` → `200 ok`, with no actor woken
  and nothing about what is mounted in the body (see
  [`health`](/docs/agent-sdk/api-reference/mount#mountoptions)).
* A path outside `base` → [`fallback`](/docs/agent-sdk/api-reference/mount#mountoptions), or
  `404`.
* An address the mount owns but cannot resolve — an unknown mount key, a
  malformed address, an empty name → `404`, one body for all of them.
* A name that will not decode, one whose actor id would exceed
  [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) characters once escaped, or one with no
  faithful actor id → `400`, with a message naming the problem.
* A request whose shape matches no transport (a `PUT`, a `GET` on an RPC
  address, an upgrade to an RPC address) → `405` with an `Allow` header. No
  actor is woken for any of them.

Refusals from the mount carry a JSON body: `\{"error": "not_found" |
"method_not_allowed" | "malformed_name", "message": "…"\}`. Answers from the
agent — including its own errors — are relayed unchanged.

**What it does and does not decide**

The mount adds **no policy of its own**. It routes, and it runs the one
[`authorize`](/docs/agent-sdk/api-reference/mount#mountoptions) callback you give it — which
runs identically on every transport, so no request shape reaches an agent
without passing it. Everything past that is the agent's: its
`authorize(token, req)` resolves claims, its `onAttach` may still veto, and
a mounted agent that overrides no connection seam still refuses sockets
exactly as it would unmounted.

Two things worth knowing before you trust a header inside an agent:

* Headers `authorize` stamps **overwrite** what the caller sent under the
  same name, so a stamped identity cannot be forged.
* Headers it does not stamp are forwarded **as the caller sent them**. Trust
  only what you stamp.

The map is read once per request and never cached, so a binding is resolved
from the `env` of the request that uses it and nothing is held between them.
Mount names are ordinary path segments, not secrets: they are part of the
address every legitimate client already knows.

**Type Parameters**

| Type Parameter | Description                                                 |
| -------------- | ----------------------------------------------------------- |
| `E`            | Your bindings type (the `env` the function is called with). |

**Parameters**

| Parameter | Type                                                                       | Description                                                                                                                   |
| --------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `map`     | [`AgentMountMap`](/docs/agent-sdk/api-reference/mount#agentmountmap)\<`E`> | The agents this function serves, keyed by mount name; see [AgentMountMap](/docs/agent-sdk/api-reference/mount#agentmountmap). |
| `options` | [`MountOptions`](/docs/agent-sdk/api-reference/mount#mountoptions)\<`E`>   | Base path, the `authorize` policy, health routes, fall-through, and cross-origin opt-in.                                      |

**Returns**

The handler to expose as your function entry's `fetch`.

(`req`, `env`) => `Promise`\<`Response`>

## AgentMountMap

> **AgentMountMap**\<`E`> = (`env`) => `Readonly`\<`Record`\<`string`, [`ActorNamespace`](/docs/edge-compute/stateful-actors/api-reference/namespace) | `undefined`>>

The agents this function serves, keyed by the mount name that addresses
them. Written as a function of `env` so the bindings resolve per request:

```ts theme={null}
mountAgents((env) => ({ support: env.SUPPORT, billing: env.BILLING }));
```

The map is the entire routing table — the mount discovers nothing on its
own and derives no names from class names, so an agent is reachable only
once it is written here.

**Type Parameters**

| Type Parameter | Description                                                 |
| -------------- | ----------------------------------------------------------- |
| `E`            | Your bindings type (the `env` the function is called with). |

**Parameters**

| Parameter | Type |
| --------- | ---- |
| `env`     | `E`  |

**Returns**

`Readonly`\<`Record`\<`string`, [`ActorNamespace`](/docs/edge-compute/stateful-actors/api-reference/namespace) | `undefined`>>

## MountOptions

Options for [mountAgents](/docs/agent-sdk/api-reference/mount#mountagents).

**Type Parameters**

| Type Parameter |
| -------------- |
| `E`            |

**Properties**

**authorize?**

> `optional` **authorize?**: (`req`, `route`) => [`MountAuthorizeResult`](/docs/agent-sdk/api-reference/mount#mountauthorizeresult) | `Promise`\<[`MountAuthorizeResult`](/docs/agent-sdk/api-reference/mount#mountauthorizeresult)>

Your one line of policy: decide whether this request may reach the agent,
and what identity to stamp on it.

Runs after the address resolves and BEFORE anything touches an actor, on
every transport — a WebSocket upgrade, an SSE stream, and each RPC POST
alike. Return a `Response` to reject (nothing is forwarded and no actor is
woken), a [MountHeadersInit](/docs/agent-sdk/api-reference/mount#mountheadersinit) to admit and stamp, or nothing to
admit as-is.

```ts theme={null}
authorize: async (req, { mount, name }) => {
  const user = await verify(req.headers.get("authorization"));
  if (!user) return new Response("unauthorized", { status: 401 });
  return { "x-desk-user": user.id };
}
```

This is edge authentication, and it **composes with** the agent's own
`authorize(token, req)` seam rather than replacing it: the stamped
headers are added to the forwarded request, the caller's
credential (`?token=`, or an `Authorization` header) rides through
untouched, and the agent still resolves its own claims from both. Stamp
identity here; decide grants there.

A stamped header always wins: it 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 the ones you stamp.

A throw from this callback is not an admission: it propagates, and the
request never reaches an actor.

Reading the body here is safe — verify a signature over it, or check an
RPC's argument shape. The agent is handed an independent copy, so a body
consumed here still arrives there intact. That copy is made only for a
request that carries a body (never for a WebSocket upgrade or a GET), and
it does mean the body is buffered while both copies are outstanding: on a
large streamed upload, prefer deciding from the headers.

**Parameters**

| Parameter | Type                                                           | Description                                                                                                      |
| --------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `req`     | `Request`                                                      | The inbound request, unmodified.                                                                                 |
| `route`   | [`MountRoute`](/docs/agent-sdk/api-reference/mount#mountroute) | What the mount resolved: the mount key, the decoded name, the actor id, the transport, and (for RPC) the method. |

**Returns**

[`MountAuthorizeResult`](/docs/agent-sdk/api-reference/mount#mountauthorizeresult) | `Promise`\<[`MountAuthorizeResult`](/docs/agent-sdk/api-reference/mount#mountauthorizeresult)>

***

**base?**

> `optional` **base?**: `string`

The path prefix the mount owns.

Every address is `<base>/<mount>/<name>`. A leading slash is optional and
a trailing one is ignored, so `"agents"`, `"/agents"`, and `"/agents/"`
are the same base. Use `"/"` to mount at the root.

**Default Value**

`"/agents"`

***

**cors?**

> `optional` **cors?**: [`MountCorsOptions`](/docs/agent-sdk/api-reference/mount#mountcorsoptions)

Opt in to cross-origin access, for a browser page served from another
origin (an `EventSource` on the SSE address, a `fetch` on the RPC one).

Absent — the default — the mount sends no CORS headers at all and answers
`OPTIONS` with `405`, so a cross-origin page cannot reach it. This is a
passthrough and nothing more: it does not authenticate, and it never
substitutes for `authorize`.

***

**fallback?**

> `optional` **fallback?**: (`req`, `env`) => `Response` | `Promise`\<`Response`>

What to do with a request whose path is not under `base`.

This is how the mount composes inside a hand-written `fetch`: the paths
you own fall through to your handler, the agent addresses do not.

```ts theme={null}
export default { fetch: mountAgents(map, { fallback: myRoutes }) };
```

**Parameters**

| Parameter | Type      |
| --------- | --------- |
| `req`     | `Request` |
| `env`     | `E`       |

**Returns**

`Response` | `Promise`\<`Response`>

**Default Value**

a `404` — the mount answers for the whole function.

***

**health?**

> `optional` **health?**: `boolean`

Answer `/health/liveness` and `/health/readiness` with `200 ok`.

These are answered by the mount itself — no actor is addressed, no
`authorize` runs, and the body carries nothing about what is mounted.
Set `false` when the surrounding function owns its own health routes.

**Default Value**

`true`

## MountRoute

What the mount resolved from the request, handed to
[`authorize`](/docs/agent-sdk/api-reference/mount#mountoptions) so the policy can decide with
the routing already done.

**Properties**

**actorId**

> `readonly` **actorId**: `string`

The actor id this name resolves to — [`encodeAgentName(name)`](/docs/agent-sdk/api-reference/mount#encodeagentname).

***

**method?**

> `readonly` `optional` **method?**: `string`

The method name, for a `rpc` request only.

***

**mount**

> `readonly` **mount**: `string`

The mount key from the address — a key of the map you returned.

***

**name**

> `readonly` **name**: `string`

The routing name from the address, decoded (`"+15550100"`, not `"%2B15550100"`).

***

**transport**

> `readonly` **transport**: [`MountTransport`](/docs/agent-sdk/api-reference/mount#mounttransport)

The transport the request's shape selected.

## MountCorsOptions

Cross-origin passthrough for [MountOptions.cors](/docs/agent-sdk/api-reference/mount#mountoptions) — opt-in, never a default.

**Properties**

**credentials?**

> `optional` **credentials?**: `boolean`

Allow credentialed requests (cookies, TLS client certs). Requires a
concrete `origin`, never `"*"`.

**Default Value**

`false`

***

**headers?**

> `optional` **headers?**: readonly `string`\[]

Request headers a cross-origin caller may send, beyond the CORS-safelisted
ones.

**Default Value**

`["authorization", "content-type", "last-event-id"]` — what
the RPC and SSE addresses actually read.

***

**maxAge?**

> `optional` **maxAge?**: `number`

How long (seconds) a browser may cache the preflight answer.

**Default Value**

```ts theme={null}
unset — the browser's own default
```

***

**origin**

> **origin**: `string` | readonly `string`\[]

The origins allowed, matched exactly. `"*"` allows any origin and is
refused in combination with `credentials`, which browsers reject anyway.

## MountAuthorizeResult

> **MountAuthorizeResult** = `Response` | [`MountHeadersInit`](/docs/agent-sdk/api-reference/mount#mountheadersinit) | `void`

What [`authorize`](/docs/agent-sdk/api-reference/mount#mountoptions) may answer:

* a `Response` — reject; no actor is woken and nothing is forwarded;
* a [MountHeadersInit](/docs/agent-sdk/api-reference/mount#mountheadersinit) — admit, stamping those headers onto the
  forwarded request (each one overwrites any same-named header the caller
  sent);
* nothing — admit, stamping nothing.

## MountHeadersInit

> **MountHeadersInit** = `Headers` | `Record`\<`string`, `string`> | \[`string`, `string`]\[]

The headers [`authorize`](/docs/agent-sdk/api-reference/mount#mountoptions) may stamp — the
same shapes the `Headers` constructor accepts, so the ergonomic literal is
the common case and a built `Headers` works when a policy needs one:

```ts theme={null}
return { "x-desk-user": user.id };            // object literal
return [["x-desk-user", user.id]];            // entry pairs
return new Headers({ "x-desk-user": id });    // a built Headers
```

## MountTransport

> **MountTransport** = `"websocket"` | `"sse"` | `"rpc"`

The transport a request selected, as resolved from its shape (never from the
path): a WebSocket upgrade, a GET for Server-Sent Events, or a POST to
`…/rpc/<method>`.

## Names

A routing name is what a caller writes in the address; an **actor id** is what the
platform files an instance's state and timers under, and only ASCII letters, digits,
`-`, `_`, and `.` are addressable. `mountAgents` runs every name it resolves through
`encodeAgentName`, so natural names — a phone number, an email address, a composite
key, a name with accents or emoji — reach a persistent instance without the caller
thinking about encoding. Call the same function from any front door you write by hand
that must address the same instances; re-deriving the rule points a name at a
different, empty instance.

The ceiling is on the id **once URL-escaped**, not on the name — see
[MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) on
the Limits page for what each kind of character costs (225 ASCII, 54 two-byte, 36
three-byte, 27 emoji, plus 6 per escaped run) and why an over-long id fails only after
an eviction rather than on first use.

## encodeAgentName()

> **encodeAgentName**(`name`): `string`

Encode a routing name into the actor id that addresses its instance.

An actor id is not free-form. It is a path segment on every call to the
instance, it is folded into the persistent records the instance owns, and it
names the instance's timers — and only ASCII letters, digits, `-`, `_`, and
`.` survive all three. An id built from anything else is not merely ugly: it
cannot be addressed, and the instance never activates.

Names customers actually route on are not confined to that set — a phone
number (`+15550100`), an email address, a composite key, a name with accents
or emoji. This encoder maps any such name onto the addressable set, and back
again:

```ts theme={null}
encodeAgentName("alice");        // "alice"          (already addressable — unchanged)
encodeAgentName("case-42");      // "case-42"        (already addressable — unchanged)
encodeAgentName("v1.2.3");       // "v1.2.3"         (already addressable — unchanged)
encodeAgentName("+15550100");    // ":2b:15550100"
encodeAgentName("a/b");          // "a:2f:b"
encodeAgentName("user@ex.com");  // "user:40:ex.com"
encodeAgentName("café-☕");       // "caf:c3a9:-:e29895:"
```

**The scheme**

Each run of characters outside the addressable set becomes `:` + the
lowercase hex of that run's UTF-8 bytes + `:`. Characters inside the set are
written through untouched. `:` itself is never written through — it is
always escaped (`:` → `:3a:`) — which is what makes an escape impossible to
confuse with an ordinary name and the encoding reversible by
[decodeAgentName](/docs/agent-sdk/api-reference/mount#decodeagentname).

Two names therefore never share an id: the id says exactly which characters
were escaped, so it decodes back to one name and one name only.

**How long a name may be**

The limit is on the ID once URL-escaped, not on the name: an actor id may
occupy at most [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) characters escaped, and a
name whose id would be longer is refused here rather than handed back as an
id the platform cannot keep an instance under.

**So how many characters of name fit depends on which characters they are**,
because an escape is longer than what it replaces — two hex digits per UTF-8
byte, plus the two `:` bracketing each run, which cost three apiece once
URL-escaped:

```ts theme={null}
encodeAgentName("a".repeat(225));  // 225 characters in, 225 out — the most a name can be
encodeAgentName("☕".repeat(36));   // 36 in, 218 out, 222 escaped — inside the limit
encodeAgentName("☕".repeat(37));   // throws: 228 escaped characters of id
```

An all-ASCII name (letters, digits, `-`, `_`, `.`) gets the full 225, since
it passes through unchanged and has nothing to escape. Everything else costs
more: 4 characters per accented Latin letter (54 of them fit), 6 per
three-byte character such as `☕` or most CJK (36 fit), and 8 per emoji or
other astral character (27 fit), with a further 6 for each separate run of
them. Budget for the characters your names actually use, use
[escapedActorIdLength](/docs/agent-sdk/limits#escapedactoridlength) when you need the exact number, and prefer
catching the throw over assuming a name fits.

**This encoding is part of an actor's identity, and is frozen**

The id an instance is first addressed by is the id its persistent state, its
timers, and every later request are filed under. **Changing how a name
encodes re-points that name at a different, empty instance and orphans
everything the old one held.** Treat the mapping as fixed for the lifetime
of the deployment: the same name must always yield the same id. It will not
be changed under you, and you should not re-derive it — call this function
instead.

That is also why [mountAgents](/docs/agent-sdk/api-reference/mount#mountagents) applies it to every name it resolves: a
mounted agent needs no encoding code, and a hand-written front door that
addresses the SAME instances stays in step by calling this rather than
writing the rule out again.

Names already inside the addressable set pass through byte-for-byte, so
adopting the encoder never re-points an instance that was already reachable.

**Two things to get right**

Takes the RAW name, **exactly once**. Encoding an already-encoded name
escapes its `:` signs (`":2b:15550100"` → `":3a:2b:3a:15550100"`) and
addresses a different instance — pass the name as the caller wrote it, not a
value read out of a URL path without decoding it first.

Letter case is significant: `Alice` and `alice` are different names and
different instances. Lowercase your names before you route on them if you
mean them to be the same.

**Parameters**

| Parameter | Type     | Description                                                                         |
| --------- | -------- | ----------------------------------------------------------------------------------- |
| `name`    | `string` | The routing name, decoded — the value a caller means, not a URL-encoded form of it. |

**Returns**

`string`

The actor id for that name, built only from ASCII letters, digits,
`-`, `_`, `.`, and `:`.

**Throws**

`TypeError` when the name has no faithful id and encoding it would
hand back one that cannot be addressed: an empty name, one whose id would
occupy more than [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) characters once escaped
(which a name of more than that many characters always does), or one
containing an unpaired surrogate. The message names the offending input and
the limit it missed.

## decodeAgentName()

> **decodeAgentName**(`id`): `string`

Recover the routing name an actor id was encoded from — the exact inverse of
[encodeAgentName](/docs/agent-sdk/api-reference/mount#encodeagentname).

`decodeAgentName(encodeAgentName(name))` is `name`, for every name
`encodeAgentName` accepts. Use it to render an id back as something a person
reads: a log line, an admin listing, a dashboard row.

```ts theme={null}
decodeAgentName("alice");              // "alice"
decodeAgentName(":2b:15550100");       // "+15550100"
decodeAgentName("user:40:ex.com");     // "user@ex.com"
decodeAgentName("caf:c3a9:-:e29895:"); // "café-☕"
```

It is strict on purpose: an id [encodeAgentName](/docs/agent-sdk/api-reference/mount#encodeagentname) could not have
produced is refused rather than half-read. That is what makes it safe to
point at an id of unknown provenance — an id from an older encoding, or one
a caller hand-assembled, is reported instead of being passed off as a name
it never was.

**Parameters**

| Parameter | Type     | Description                                                                                     |
| --------- | -------- | ----------------------------------------------------------------------------------------------- |
| `id`      | `string` | An actor id produced by [encodeAgentName](/docs/agent-sdk/api-reference/mount#encodeagentname). |

**Returns**

`string`

The routing name it was built from.

**Throws**

`TypeError` when `id` is not something [encodeAgentName](/docs/agent-sdk/api-reference/mount#encodeagentname) could
have produced: an empty id, a character outside the addressable set sitting
unescaped, a `.` or `..` (which the encoder always escapes), two escapes in
a row, an unterminated escape, an escape that is not whole bytes of hex, or
hex that is not valid UTF-8.

## MAX\_AGENT\_NAME\_LENGTH

> `const` **MAX\_AGENT\_NAME\_LENGTH**: `225` = `MAX_ESCAPED_ACTOR_ID_LENGTH`

The longest routing name the mount will address, in characters of the
decoded (pre-encoding) name.

This is the ceiling a name can have under
[MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length), not a second rule: every character of a
name costs at least one character of the escaped actor id it encodes to, so a
name longer than this cannot possibly have an id within the limit and is
refused without encoding it. A name at or under it is **not** thereby
accepted — one character of a name can cost eight or more, so
[MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) is what actually decides, and only a name
spelled entirely from ASCII letters, digits, `-`, `_`, and `.` gets the whole
length. See [MAX\_ESCAPED\_ACTOR\_ID\_LENGTH](/docs/agent-sdk/limits#max_escaped_actor_id_length) for what other characters
cost.

A name over this length answers `400 malformed_name`.
