Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. --adapter <name[:profile]> is an optional override (since v0.9.0). For a harness-hosted session you normally omit it: listen resolves 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 by spt adapter use) or, with no pointer, the freshest-registered hosting adapter. Pass --adapter only 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 a live profile 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.
  2. 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 unauthenticated api poll is refused (exit 1, nothing printed) — see api poll.
  3. 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: --json output, 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:

  1. 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.
  2. Never skip or swallow this call. A rotation that is silently skipped (unresolvable endpoint id) or silently refused (AUTH_REFUSED on 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-download is how the next session reads that mind back in. Wire it into your SessionStart hook alongside seed/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

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:

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.

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.

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 as CODE:detail. Parse lines, not prose.
  • Exit codes: 0 success; 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.md into the manifest’s watched directory; spt-core’s watcher ingests it. There is deliberately no api commune.