Skip to main content

What is the user scope?

A thread belongs to one conversation. The user scope belongs to one user and spans every thread that user owns. It has five parts:
  • User endpoints: HTTP routes that run for one user
  • User values: durable JSON values for one user
  • User event stream: a WebSocket that delivers live events to the user’s connected clients
  • User room: an optional WebSocket hub that receives frames from the user’s clients
  • Credential grants: credentials that the runtime adds to requests for the user
Use the user scope for data that more than one thread reads: preferences, quotas, counters, and registries.

User endpoints

Quick start

Create a file in agents/users/ and export a defineUserEndpoint call as the default export:
The endpoint is available at GET /api/users/me/preferences. Standard Agents Builder discovers it from the agents/users/ directory.

File-based routing

Routing follows the rules of thread endpoints. The path sets the route, a suffix sets the method, and [name] and [*] capture path segments. me is the authenticated caller. Every route under /api/users/me/ requires authentication. A super admin session has no user account, so a me route returns HTTP 400 for it. The paths env, kv, hub, and stream directly under /api/users/me/ belong to built-in routes. Do not name a user endpoint file after one of them. A request that matches no user endpoint returns HTTP 404.

Handler function

  • req: the Web API Request
  • user: the UserState of the addressed user
  • params: the values captured from [name] and [*] path segments
A user endpoint receives no ThreadState. An exception that the handler does not catch returns HTTP 500.

One run at a time for each user

For one user, Standard Agents Builder runs one user endpoint at a time. Each run sees every write that earlier runs completed. A user endpoint can therefore read a value, check a limit, and write the new value without a race:
This guarantee covers user endpoints and state.user().call(). It does not cover direct value writes from thread code, such as (await state.user())?.setValue(...).

UserState

User values are plain JSON. Do not store credentials in them. Store a credential for an external API as a credential grant.

Calling a user endpoint from a thread

Tools, hooks, handlers, and thread endpoints reach the thread’s user through state.user(). call() runs a user endpoint by its path under agents/users/, with the same one-at-a-time guarantee.
  • call() sends a POST request with the payload as a JSON body. It reaches endpoints that handle POST.
  • call() rejects a path that contains a . or .. segment, in plain, percent-encoded, or backslash form.
  • state.user() returns null for a thread without a user.
  • A ThreadState that came from user.thread() cannot use call(). The call throws, because the user endpoint that raised the thread is still running.
The value returned by state.user() also has getValue(), setValue(), emit(), and fetch() for the same user.

Caller channel

Standard Agents Builder sets the X-Standard-Agents-Caller request header on every user endpoint request. The public routes overwrite any value that the client sent. Check for thread in an endpoint that only your agents may change:

Administrative access

An administrator can call the same endpoints for another user at /api/users/:id/.... The endpoint runs in the addressed user’s scope. A caller that is not an administrator receives HTTP 403. To guard an endpoint with application roles, see application role checks.

User event stream

The user event stream delivers every user.emit() and state.user().emit() event to the user’s connected clients.

Connecting

A browser cannot set an Authorization header on a WebSocket. Request a single-use ticket for the exact path and pass it as ?ticket=, as the thread stream does:
A request without a WebSocket upgrade returns HTTP 426. A user can hold several connections at the same time, one for each device or tab.

Frames

Each frame is a JSON object:
  • Standard Agents Builder sends { "event": "stream_ready", "data": { "client_id": "..." } } as the first frame of each connection. Pass ?client_id= to set that ID, and ?client_kind= to label the client.
  • data is absent when the event was emitted without data.
  • Frames arrive in the order the user’s code emitted them.
  • Choose event names that include your package or feature name, such as quota_changed.

Delivery

The stream is live only. It stores no history and has no acknowledgements. A client that was not connected when an event was emitted does not receive that event. After each connect and reconnect, read the current state from your user endpoints. Use the stream for changes that occur after that read.

Keepalive

Send the string stream_ping at intervals. The runtime answers stream_pong.

User rooms

What is a room?

A user room is a WebSocket hub for one user’s connected clients. A project has at most one room. The room adds three things to the user event stream:
  • Client frames: a client sends a frame on the socket, and your code handles it in the user scope
  • Targeted delivery: your code sends a frame to one client by its ID
  • Presence callbacks: your code runs when a client attaches or detaches
Room frames and presence callbacks run one at a time for each user, in the same queue as user endpoints.

When a project needs a room

Define a room when one of these is true:
  • Clients must send messages to your user-scope code over an open socket.
  • Your code must address one device or tab of the user.
  • Your code must know which clients are connected.
  • Your code must verify a client before that client receives frames.
The user event stream is enough when your code only pushes events to all of the user’s clients and the clients send their changes through user endpoints.

Defining a room

Create agents/users/<package>/room.ts and export a defineUserRoom call as the default export:
  • onFrame runs for each room frame that a client sends.
  • onPresence is optional. It runs when a client attaches and when a client detaches. In the default admission mode, a client that reconnects within 30 seconds causes no presence change.
  • A room file is not an HTTP route. A request to its path returns HTTP 404.
  • When two files define a room, Standard Agents Builder refuses room connections with HTTP 503. User endpoints and the user event stream continue to work.

Hub route

Authenticate with a single-use ticket for the exact path, as the user event stream does. A request without a WebSocket upgrade returns HTTP 426.
  • Standard Agents Builder sends { "type": "hub_ready" } as the first frame.
  • client_id identifies the client. A connection without client_id is not a room connection. The optional client_kind labels the client. Each value is trimmed and cut to 256 characters.
  • A room frame is a JSON object with "type": "room" and a string id of 1 to 256 characters, such as { "type": "room", "id": "1", "op": "ping" }. The other fields belong to your protocol. The runtime sends no acknowledgement, so reply with user.send() and repeat the id.
  • A room frame in a project without a room receives { "type": "room", "id": "...", "error": "no_room" }.
  • user.emit() events also reach room connections, as { "event": "...", "data": ... } frames.
  • In the default admission mode, a new connection with the same client_id replaces the earlier one. The earlier socket receives { "type": "superseded" } and closes with code 4001. A client that receives this frame stops its reconnect attempts.

Admission rules

A room has its own admission mode. The default is automatic: each connection with a client_id joins the room at once. With admission: 'explicit', a new connection starts as pending. A pending connection receives no user.send() frames and no user.emit() events, and user.clients() does not list it. onFrame still receives its frames, so your code can verify a proof from the client. Reply to a pending connection with user.roomAttachments.send(), admit it with user.roomAttachments.admit(), or reject it with user.roomAttachments.close(). Each call takes connection.attachmentId. An admitted connection replaces the earlier admitted connection with the same client_id. In this mode a connection without client_id returns HTTP 400. Room connections do not run the user_client_attached and user_client_detached hooks. Those hooks belong to the user event stream.

User lifecycle hooks

Three hook types run in the user scope. Each receives a UserState in place of a ThreadState. client is { id: string; kind?: string }.
A user hook needs no reference from a prompt or an agent. It runs for every user of the project while its file exists. A user hook observes the operation and cannot block it. Standard Agents Builder discards an error that a user hook throws.

Next steps

API

Create endpoints that operate on one thread

Credential grants

Call an external API with a user credential that your code cannot read

Thread

Read the thread owner’s identity with state.user()

WebSocket stream

Receive live frames for one thread

Hooks

Run code at thread lifecycle points