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.
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 —/agentsunless you setbase.<mount>is a key of the map above.<name>is the routing name of one instance — one agent, one persistent identity.
/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:
result / error
frame a WebSocket caller would receive. Only methods decorated with
@rpc() are callable.
Authorization composes — it does not replace
The mount adds no policy of its own. It routes, and it runs the oneauthorize
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.
- 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.
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
encodeAgentNamefrom 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.
aliceandAliceare 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:
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.
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
Absentcors, 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:
authorize. Mount names are ordinary path segments, not secrets — they are part of
the address every legitimate client already knows.
Next
mountAgentsreference — every option, type, and refusal in full- AgentClient — the browser/Node client that connects to the address above
- AgentSocketServer and AgentHttpServer — what answers on the agent side of each transport
- WebSockets — the platform surface underneath, and what a hand-written front door looks like