The spt api surface
The imperative half of the harness contract: the inbound entry points a harness’s hooks (and a shell’s binary) fire to keep spt-core’s on-disk state in sync. This page is the complete command reference plus the two startup flows that tie it together.
Three rules apply to api calls:
--adapter <name[:profile]>is an optional override (since v0.9.0). For a harness-hosted session you normally omit it:listenresolves the owning adapter/profile at bind, from the seed’s parent pid → the harness exe basename → the adapter(s) that declare it in[adapter] host_binaries→ the active-profile pointer (set byspt adapter use) or, with no pointer, the freshest-registered hosting adapter. Pass--adapteronly to pin a specific adapter/profile (adapter dev, or explicit disambiguation). The profile qualifier<adapter>:<profile>is runtime selection — retained onto the perch record, and the daemon resolves the profile overlay when it later spawns the session’s lifecycle roles. So aliveprofile whose[session.psyche_init]is in the resolved manifest is a LiveAgent (spt-core drives the Psyche as a bounded per-event turn); a profile without it is a ReadyAgent. Ready-vs-live is a profile choice, not a separate “go-live” verb.- Prove association. Commands that touch an existing perch take
--session-id <id>(matching the perch’s record) or a capability--token; shell commands authenticate with--link <token>(the link token minted at launch is the credential — no token, no access). This includes the read-side drain: an unauthenticatedapi pollis refused (exit 1, nothing printed) — seeapi poll. - Status rides stderr; stdout is payload; the exit code is authoritative.
Action-command status lines (
BOUND:<id> token=…,READY:<id>,SENT:,QUEUED:,WORKER_STARTED:…, failure tags) print to stderr — always, piped or not (only the color is tty-gated; a redirect gets the bare tag unchanged). stdout is reserved for machine payloads:--jsonoutput, polled message frames, and documented payload emissions. A program shelling out must therefore capture stderr to read a status tag (2>&1, or capture the streams separately) — discarding stderr (2>$null/2>/dev/null) discards the status line by design — and should treat the exit code as the success contract:0= the action took effect, non-zero = it did not.
spt api [--adapter <name[:profile]>] [--manifest <path>] <command> …
--manifest points at the adapter’s manifest for the commands that need it
(e.g. capability).
The two startup flows
Harness-hosted — the harness owns the process; spt-core is invoked from inside it (hooks):
SessionStart hook ──► api seed --pid {parent_pid} --session-id {session_id}
session's listener ──► api listen <id> (consumes the seed, holds the perch)
seed records an ephemeral hand-off keyed by parent pid; listen consumes
it, registers the perch, drains backlog, and blocks relaying events into the
session.
spt-hosted — spt-core spawns the session itself from the manifest’s
[session.self] template, in its own terminal layer:
spt-core spawns the template ──► session comes up
session (or its wrapper) ──► api bind <id> --set-session-id <discovered-id>
No seed file is involved; bind attaches the live session to its perch
post-spawn.
Going ONLINE (the presence badge). The endpoint list ONLINE badge means
one thing: a live process is holding the relay — an api listen <id>
that consumed a seed and is blocked relaying events. Binding alone does not
light it. A headless adapter binary (a gateway or any [session.self] host
that wants ONLINE presence and a live event stream) uses the same two-step
the harness-hosted flow does, against its own process:
api seed --pid <own-pid> --session-id <sid> (hand-off keyed to itself)
api listen <id> (consume, hold the perch, stay ONLINE)
Drop the listener and the endpoint decays to Dormant/Offline as its last-seen ages out.
Session lifecycle
api seed --pid <pid> --session-id <id>
Harness-hosted startup, step 1: record an ephemeral seed keyed by the parent
process id. Fired by the harness’s session-start hook. Prints SEEDED:<pid>.
api listen <id> [--once] [--parent-pid <pid>] [--subnet <name>]
Harness-hosted startup, step 2: consume the seed, register/hold the perch,
drain spooled backlog, then block relaying messages. --once runs a single
drain+receive cycle (testing). --subnet names the home subnet when this
creates a brand-new endpoint on a multi-subnet node (home is assigned
deterministically at creation).
api bind <id> [--set-session-id <sid>]
spt-hosted startup: bind a freshly spawned session to its perch, recording the session id discovered post-spawn. Identity precedes sessions — rebinding never mints a new endpoint.
Auth is intrinsic — bind takes no association proof. It is an
establishing call (the exception to Rule 2), not a touch-an-existing-perch
call: spt-core spawned this session into its own broker-held terminal layer, so
that parentage is the credential. The only guard is ownership — an existing
live perch under a different session id is refused (you can only bind your
own). The broker injects no capability token into the spawned environment,
so there is nothing to echo back and no [env.*] entry to author for one; the
endpoint id arrives via the {id} fill in [session.self], and that is the
only identity spt-core plants. --set-session-id records the discovered id
into the perch — it is not a proof.
bind prints BOUND:<id> token=<token>. The token is a freshly minted local
credential the session may retain for later authenticated calls, but it is
optional: every subsequent mutating call can instead prove association the
Rule 2 way, passing --session-id <that same id> for spt-core to match against
the record this bind wrote.
api boundary <clear|compact> <id> --to-session-id <new-sid> --session-id <prior-sid>
The session was reset (context cleared or compacted) and continues under a new session id: rebind the perch, preserving the endpoint’s identity, spool, and history across the boundary.
The rotation catch-22 — read this before wiring the hook. Rule 2 applies to
boundary like every mutating verb, but here the “matching” --session-id is
the sid on the perch record — i.e. the session being departed, not the one
you are rotating to. --to-session-id is the payload, never the proof. By
the time your rotation hook fires, the old session’s context (its env, its
per-session files) is typically gone and the hook payload carries only the new
sid — so a hook that only knows “the current sid” cannot authenticate this one
verb. Two hard requirements for adapter authors:
- Persist the current session id at every SessionStart in adapter-owned
state keyed by the endpoint id (reference pattern:
{adapter_dir}/state/session/<endpoint_id>.sid, single line, rewritten each SessionStart). At clear/compact, read it back as the prior sid and pass it as--session-id. Do not persist it in a per-session env file — that file dies with the session, which is the catch-22 itself. - Never skip or swallow this call. A rotation that is silently skipped
(unresolvable endpoint id) or silently refused (
AUTH_REFUSEDon stderr, exit 1) leaves the perch pinned to the dead sid — after which every id-scoped call from the live session refuses, including a boundary retry, and the endpoint strands with zero message delivery until a full relaunch. Surface the id-resolution failure and the refusal reason loudly in your hook output. Resolve the endpoint id from your stable identity ($SPT_ENDPOINT_ID), not by looking up the new sid (it is not registered yet — the same catch-22).
A perch stranded by a crashed session (recorded pid dead) self-heals on the
next call via the dead-owner re-pin; a live session’s rotation always
requires the prior-sid (or --token) proof. The long-term design (ADR-0032)
adds an OS-verified ancestry proof keyed on the endpoint’s stable parent_pid
anchor, which will make the persisted-sid pattern optional; until then it is
required.
api psyche-download <id> [--session-id <sid>]
Pull the agent’s resume context to stdout, for a SessionStart hook to
inject as the session’s additional context after a /clear, /compact, or
fresh resume. Emits the durable two-tier mind — the agent’s role, its
cross-project live context, and the current project’s context — plus any
commune/signoff drop that has been written but not yet synthesized into
that durable context (as <pending-commune> / <pending-signoff> slices), so
a just-written delta is never invisible on resume. The project is resolved
from the perch’s recorded cwd. Read-only — it never writes the mind store.
Prints NO-CONTEXT:<id> on stderr (exit 0) when nothing is stored yet, the
adapter’s fresh-init signal.
The read-back-in half of the commune/signoff file-drops (the write side): the agent drops its delta, spt-core synthesizes it into the durable mind, and
psyche-downloadis how the next session reads that mind back in. Wire it into your SessionStart hook alongsideseed/listen, and inject its stdout — that is how a resumed session keeps its accumulated context.
api session-end <id> [--erase]
Soft teardown: the session is over; the perch’s spool and history are
preserved (that’s what makes the next poll/listen drain work). --erase
hard-wipes instead — the exception, not the rule.
api shutdown <id>
Graceful live-agent signoff: runs the final echo-commune before teardown
(the context delta is never lost to ordering), then soft-stops. This is what
the spt endpoint shutdown lifecycle path calls.
Activity and presence
api state <busy|idle> <id> [--no-gate]
Report the session’s activity state. Activity/idleness comes from these
explicit reports — never from terminal quiescence, which lies. Reporting
idle also arms the echo gate (below) unless --no-gate.
api echo-gate <set|clear> <id>
Manage the echo-gate sentinel directly. The gate marks “a summarization may
be needed when this session ends without a graceful signoff” — state idle
sets it as a side effect; a graceful signoff clears it.
api presence <id>
Report user/agent presence at this endpoint (feeds most-recently-active resolution across the subnet).
api driven-by <id>
Print which node (if any) is currently remote-driving this endpoint, so a session can tell whether input is local or remote.
Messages
api poll <id> [--include-deferred] [--link <token>]
Drain delivered messages over the hook channel (the pull-based path for
harnesses whose hooks can’t inject). Deferred-flagged rows are excluded
unless --include-deferred. With --link this is the shell-flavored drain:
the link token authenticates, and the rows are the shell’s stamped
command/text/file frames.
Authentication is required (rule 2 above): the drain must prove
association with --session-id <sid> (the perch’s recorded session) or a
capability --token (--link <token> for the shell flavor). An
unauthenticated poll is refused with exit 1 and no output — messages
are addressed to the endpoint’s occupant, not to whoever asks.
api history-log <id>
Append normalized history (body on stdin) to the endpoint’s native history
store — the push half of [history] strategy = "native".
Workers
Nested, short-lived agents under a parent endpoint. A worker is process-local machinery — it authenticates with its parent’s session id and carries no capability token of its own.
api worker-start <parent> [--agent-id <id>] [--agent-type <type>]
Create a nested worker perch under parent. The worker id is minted by
spt-core, not supplied by the caller: {parent}-w{N} with a persistent,
per-parent counter. The caller does not pass an id (a stray positional id is
rejected).
Output channels follow the api status-line discipline:
- stdout carries the bare minted id and nothing else (the machine-readable result — empty on any refusal). Read this to learn the worker’s id.
- stderr carries the human line
WORKER_STARTED:{parent}-w{N} under {parent}.
--agent-id / --agent-type are optional: the caller’s own agent identifiers,
recorded on the worker as correlation metadata only — never the perch
identity.
Authenticates against the parent (the parent’s session id or token); the worker record stores the parent’s current session id as its registration sid.
api worker-stop <id> --session-id <sid> · api worker-poll <id> --session-id <sid>
Soft-stop (drop the ready marker; info + spool preserved) or drain a worker.
Both authenticate symmetrically by session id — no token. A presented sid is
accepted when it matches either the worker’s stored registration sid or
the parent’s current session id, so a context clear/compact that rotates the
parent’s sid between start and stop does not lock the worker out. The natural
call worker-stop <id> --session-id <parent sid> is therefore correct as-is.
Shells
The driven-surface flavor of the contract. The link token minted at launch is the only credential a shell binary ever holds or needs:
api bind-shell --link <token>
The shell binary’s first call: resolve the instance by link token alone
(the spawn template carries only {link_token}; the owner is derived from
the link) and flip it online.
api emit <id> <payload> --type <type> --link <token>
Push a sensory payload (one of the manifest’s declared [shell.sensory]
types) to the owner’s live session. REST-only by definition: never
spooled — if the owner isn’t live, it’s dropped with a diagnostic. Sensors
report the present, not the past.
api owner-shutdown <id> --link <token>
A shell suspends its linked owner directly (e.g. a power-button surface),
bypassing agent messaging. Gated by the manifest’s can_shutdown
pre-consent flag — fail-closed; an undeclared shell gets a refusal. The
firing shell cascades offline with its siblings, by design.
Introspection
api capability
Print the adapter’s declared hostable_types (requires --manifest). The
cheap way to smoke-test that spt-core reads your manifest the way you meant
it.
Conventions
- Output is line-oriented and stable:
SEEDED:<pid>,READY:<id>,SENT:<id>,QUEUED:<id>, error lines asCODE:detail. Parse lines, not prose. - Exit codes:
0success; non-zero = refused or failed, with the reason on stderr. - Commune/signoff are file-drops, not api commands. An agent writes
<endpoint_id>-commune.md/<endpoint_id>-signoff.mdinto the manifest’s watched directory; spt-core’s watcher ingests it. There is deliberately noapi commune.