Addressing
Every running agent is an entity with one URL-shaped id, and every stream and workspace it uses derives from that id. This page is the grammar and the derivation rules; for the mental model, read Entities & addressing.
All the derivation functions live in @teaspill/schema (addressing.ts). They are pure and validate their inputs — a malformed identifier throws AddressingError rather than reaching the catalog or a stream.
At a glance
| Thing | Canonical form | Example (default tenant) |
|---|---|---|
Entity URL (entityId) | /t/<tenant>/a/<type>/<id> | /t/default/a/researcher/01j9z8k3q… |
| Entity URL, gateway short form | /a/<type>/<id> | /a/researcher/01j9z8k3q… |
| Timeline stream (server key) | /t/<tenant>/agents/<type>/<id>/timeline | /t/default/agents/researcher/01j9…/timeline |
| Deltas stream | /t/<tenant>/agents/<type>/<id>/deltas | /t/default/agents/researcher/01j9…/deltas |
| Workspace stdout stream | /t/<tenant>/workspaces/<name>/stdout | /t/default/workspaces/a-researcher-01j9…/stdout |
| Gateway stream URL | /streams + <streamPath> | /streams/t/default/agents/…/timeline |
The entity URL is the same string everywhere — the catalog row's primary key, the entityId in every event, and the value browsers hold to read a stream. There is no separate opaque id.
Segment grammar
Every segment — <tenant>, <type>, <id>, <name> — matches the same charset:
^[a-z0-9][a-z0-9_-]*$
Lowercase alphanumerics, -, and _; must start with an alphanumeric. No ., /, %, uppercase, or unicode inside a segment; / is the path separator only.
| Segment | Max length |
|---|---|
| tenant | 32 |
| type | 48 |
| id | 64 |
| workspace name | 64 |
Lowercase-only avoids "same entity, two URLs" bugs on case-insensitive filesystems, where the history store writes one file per stream. The length caps keep the full timeline stream path legible on disk.
The <tenant> segment is a deployment-level constant (TEASPILL_TENANT, default default). One deployment is one tenant, so in practice every identifier reads /t/default/…. The gateway accepts and emits the tenant-relative short form /a/<type>/<id> for ergonomics and expands it to the canonical form before touching anything durable.
Instance ids
- Default: a lowercase ULID (26 chars, time-sortable). Generated by
newInstanceId(). Sortable meansORDER BY idroughly matches creation order — convenient for listings and catalog queries. - Caller-supplied ids enable deterministic, idempotent spawn: key a child's id off
(parent, role)and a retried spawn reattaches to the same instance instead of creating a duplicate. A supplied id must match the segment grammar (validated byassertInstanceId); the gateway rejects a non-conforming id with a 400.
(tenant, type, id). A second spawn with the same id lands on the same object and reattaches — it does not create a duplicate. See Multi-agent patterns for the deterministic-worker pattern.Stream paths
Each stream is an independent append-only log. The paths below are the server keys; a client reaches them through the gateway at /streams + <path> (see the Gateway HTTP API).
| Stream | Server key | Helper |
|---|---|---|
| Timeline (authoritative history) | /t/<tenant>/agents/<type>/<id>/timeline | timelineStreamPath(url) |
| Deltas (live token fragments) | /t/<tenant>/agents/<type>/<id>/deltas | deltasStreamPath(url) |
| Workspace stdout (coarse) | /t/<tenant>/workspaces/<name>/stdout | workspaceStdoutStreamPath(key) |
| Workspace exec stdout (per-exec) | /t/<tenant>/workspaces/<name>/exec/<runId>/stdout | workspaceExecStdoutStreamPath(key, runId) |
Note the collection segment is agents (plural) in a stream path versus the a marker in an entity URL — both derive from the same entity URL. Prefix any server key with /streams for the client-facing URL: gatewayStreamUrl(streamPath).
Workspace keys
A workspace key is <tenant>/<name>. The <name> comes from one of two places:
- Private (default, one per entity):
a-<type>-<id>, derived from the owning entity URL byprivateWorkspaceKey(url). Unique by construction, traceable back to its agent. - Shared / named: a caller-supplied
<name>passed at spawn (same segment grammar). Several entities may point at the same shared key; serialization still holds because it is keyed on the workspace, not the entity.
Helpers
Exported from @teaspill/schema.
| Function | Returns |
|---|---|
entityUrl(tenant, type, id) | Canonical entity URL. |
parseEntityUrl(url) | { tenant, type, id } (revalidates). |
isEntityUrl(url) | Boolean guard. |
toHttpForm(url) / fromHttpForm(path, tenant?) | Canonical ↔ gateway short form. |
newInstanceId() | A fresh lowercase ULID. |
assertInstanceId(id) | Throws unless the id matches the grammar. |
timelineStreamPath(url) / deltasStreamPath(url) | Server stream keys. |
gatewayStreamUrl(streamPath) | Prefix a server key with /streams. |
workspaceKey(tenant, name) / parseWorkspaceKey(key) | Build / split a workspace key. |
privateWorkspaceKey(url) | The default per-entity workspace key. |
workspaceStdoutStreamPath(key) / workspaceExecStdoutStreamPath(key, runId) | Workspace output stream keys. |
The regexes (TENANT_RE, TYPE_RE, ID_RE, WORKSPACE_NAME_RE) and constants (DEFAULT_TENANT, GATEWAY_STREAMS_PREFIX) are exported too.
Internally, each identifier resolves to a Restate virtual object — the coordination primitive teaspill runs on. @teaspill/schema exports the mapping (restateAgentKey, restateWorkspaceKey, restateCronKey, steerKey), but you never call these as a teaspill user: the gateway and the platform own them. An agent object is service agent.<type>, keyed by <id>; a workspace object is service workspace, keyed by <tenant>/<name>. The tenant is not part of any Restate key — one deployment is one tenant. This surface is contributor-facing; the package map covers it.