Skip to main content
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.
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.
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.
  • <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: This is why the AgentClient 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:
An RPC call from a page with no SDK at all is the same address with a method on the end:
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() are callable.
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 page.

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: 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 resolves this connection’s claims from it exactly as it would with no mount in front, and onAttach may still veto.
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, which maps any name onto the addressable set reversibly:
/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-escapedMAX_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: 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.
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 and escapedActorIdLength().

What the mount answers itself

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:

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