createClient<typeof config>() adds the durable-runtime surface to the same object you
already use. Nothing legacy moves: NaiveClient extends Naive, so one credential, one
host, one transport.
A team is a pair, not a name
naive.teams.support is a reference, not a handle. The addressable unit of the durable
runtime is (team, tenant) — the same pair the runtime itself uses — so you bind a tenant
before you can do anything:
forTenant verifies; it does not get-or-create, and it costs one round trip to say so.
It resolves GET /v1/users/{id} first, so an unknown tenant id fails with not_found here
instead of silently succeeding and writing a run against a tenant that does not exist. There
is deliberately no unchecked twin — a twin would become the default and the check would
become decoration.naive.config.ts to type the client, naive.teams.support does not typecheck.
Use the string twin, which behaves identically at runtime:
Agents are derived, never addressed
What is wired today
Three namespaces reach mounted routes.runs — the run ledger
list() and events() both return { items, next_cursor } — keyset pages, not
{ runs, count }. list() takes { limit, cursor } and no status filter.
approvals, brain, memory — real, but not team-narrowed
What is wired, and what refuses
Nothing on this client returns{}, and nothing refuses at a route that exists.
The rule is one line: a method refuses in the client only when the route is not mounted.
When a route IS mounted the call goes out — even if the server answers 501 not_configured,
because “there is no route” and “the route refuses” are different instructions. Only the
second arrives with error.details.missing naming each absent dependency, written by the
people who know, and only the second starts working the day that dependency lands without a
new release of this package.
Wired to the (team, tenant) address
A
501 for a durable tenant means something narrower than “not built”. It names the
verb the runtime already implements and says which control-plane translation is missing, so
error.details.missing distinguishes not built from not reachable from here — a
distinction a caller can act on and a blanket 501 destroyed. Where the refusal came from
the runtime itself, its status is preserved and its own sentence is carried through verbatim
in error.details.runtime_said.submit() resolves with manifest_digest: null and manifest_digest_unavailable_because
beside it: the durable runtime reports its applied digest on GET …/plan, not on an
admission. Read the sibling; do not read the null as “no manifest”.board.stop() stops the team, not the board — the mounted route is team-scoped
(POST …/stop). It lives on board only because that is where the published surface put it.
For a durable tenant it is served: it stops the dispatcher claiming further work and
fences it against re-arming (start-loop resumes). For a hermes tenant it is 501 — a
team-level stop is a state of the Durable Object, and the legacy runtime has no equivalent
row to set; a legacy run is stopped one at a time through the sidecar.Read the answer, not just the status. It carries cancelled (scheduled ticks removed),
in_flight (attempts already handed to a member), stopped_at, and
is_pause_not_decommission: true. That last field is the point: this is a pause. Cards
keep their leases and their attempt budgets, submit() still admits work, and an attempt
already in flight is not recalled — there is no abort channel into one, so it runs to its
end and still spends. in_flight is how you know whether that number is zero. The runtime
refuses a per-run kill (POST …/runs/{id}/stop) by design, for the same reason.spend.cost({ by }) accepts exactly one value. The route throws
invalid_input: by must be "action" (the only bucketing this build can compute) on anything
else, so the parameter is typed to what it accepts.The brains, from a team handle
support.brain and support.brains are the same objects naive.brain / naive.brains
give you. They are here because a handle that offers the value but not the collection sends
you back to the root for half of one surface — not because the team or the tenant scopes
them. brainRouter reads the company id; the :tenant segment selects the mount, not the
rows.
A 501 arrives as an ordinary NaiveError with code: "not_configured", and its
details.missing is the list of things the server is waiting on:
Refuses in the client — the route does not exist
Eight methods have no mounted route and each throws a typedNotImplementedError naming
the exact METHOD /path it would have called, sending nothing. The registry is data
(MISSING_ROUTES), a test walks every namespace and asserts the refusing set is exactly
that table, and a second test asserts every row is absent from the published OpenAPI spec —
so a row cannot survive the route landing.
For anything this client does not model yet,
legacy.invoke(method, path, body?) is a raw
request on the same credential and host. It names no noun and applies no shape, which is
exactly why it is safe for a route with no handle:
The frozen legacy surface
Known gaps
- No manifest-digest fence.
defineConfigreturns your config by identity and computes no digest, so the client has nothing to send and nothing to compare. A local config’s types can therefore be newer than the applied manifest. The option is absent rather than accepted-and-ignored. naive planat project scope has no SDK equivalent, for the same reason: its central field is that digest.t1.toolsis not narrowed to the agent’scanlist — see the warning above.