capabilities

What Psych handles while an agent works.

Browse the behavior you need. Every item below exists in the runtime today, and opens to show the mechanism that keeps it reliable.

Execution

Getting a Run from admitted to finished, and back on its feet after a crash.

01

Recover a run after a worker stops

Kill a Worker mid-tool-call and another finishes the Run: it reclaims the expired lease, replays the log and settles the dangling call.

how it is kept
  • A Worker claims a Run with a lease in one conditional write. Two concurrent claims cannot both win.
  • The lease follows process liveness. A separate wall-clock deadline, enforced in the process holding the Attempt, catches the hang a lease cannot see.
  • Supervision and execution are separate loops, so a stalled model stream can never block the code that would have timed it out.
DESIGN.md §8, §23.2 · guide
02

Every read is computed from the log

Every question about a Run is a pure fold over its append-only log.

how it is kept
  • Reports are recomputed on every read and never stored twice, so they cannot drift.
  • Records are sequenced without gaps and appended under a conditional write. Exactly one Attempt holds the lease and may append.
  • The reducer has no IO, no clock and no randomness. Same log in, same state out.
  • A log the protocol could not have produced raises a typed CorruptLog with one of fourteen named reasons. It is never repaired.
DESIGN.md §6, §13 · guide
03

Agents are data, published as versions

An agent is a serialisable Spec of tool names, which publishing hashes into an immutable Version.

how it is kept
  • A Spec never holds a callable, and the same Spec built in Python or from a dict hashes the same.
  • Canonicalisation is chosen, not inherited: key order, float formatting, defaults materialised before hashing, server-assigned fields excluded.
  • A Spec is validated once, at publish. A customer waiting on a response is the wrong place to discover a typo. Per-call checks that only the runtime can make, tool narrowing, approvals and limits, still happen every turn.
  • Runs pin a Version for their whole life, so editing an agent never mutates a Run in flight.
DESIGN.md §4, §23.1 · guide
04

Four stores, one contract

The same Spec runs identically on in-memory, PostgreSQL, MySQL and DynamoDB.

how it is kept
  • The queue is the store: a runnable Run is one whose lease is unheld or expired, and there is no broker beside it.
  • Every adapter implements three primitives: append at an expected sequence, claim under a condition, read a range.
  • One contract suite runs against all four, against real databases. No store is mocked in the test suite.
  • Migrations are sequential and forward-only.
DESIGN.md §7, §23.8 · guide
05

Workflows resume from the last completed step

A workflow with a nested agent resumes from its last completed step after a crash and does not re-execute completed steps.

how it is kept
  • Steps are tool calls, agents or nested workflows, sequenced deterministically.
  • Step names are the memoisation key. A completed step's output is read back from the log rather than recomputed.
DESIGN.md §5, §23.5 · guide
06

Subagents with a depth and a fan-out limit

A parent's Spec embeds its subagents, so one Version hash pins the whole tree.

how it is kept
  • A model may also compose a child at run time, within a SpawnEnvelope that joins the hash like every other permission.
  • Tool access narrows down the tree and never widens.
  • A parent with background children and no work of its own suspends on CHILDREN rather than holding its lease.
DESIGN.md §17 · guide

Tools

What an agent can reach, and where the code it writes runs.

01

Four kinds of tool, resolved every turn

Python functions, HTTP endpoints, MCP servers and A2A peers are resolved at the start of every turn, never at boot.

how it is kept
  • A source connected mid-Run is usable on the next turn without a restart, and invisible to another tenant's Run.
  • Code tools take their schema from type hints and their description from the docstring, so neither can drift from the function.
  • HTTP tools and MCP servers are data in the Spec. End users can create them at runtime without a redeploy.
  • Three consecutive failures of one tool within a Run stop the model repeating it.
DESIGN.md §10, §23.10 · guide
02

Model-written code runs in a separate process

A program the model writes executes in a separate process, calls host tools through the normal tool path, and its traceback on failure reaches the model as data.

how it is kept
  • In-process sandboxing is rejected outright: RestrictedPython, exec with trimmed builtins and AST filtering are all escapable.
  • The two backends contain different amounts. The subprocess sets rlimits and drops to an unprivileged uid; it shares the host filesystem and its network denial is a self-report, not enforcement. The container's is a kernel guarantee.
DESIGN.md §18, §23.9 · guide
03

Memory across runs, skills loaded on demand

Facts persist across Runs under a key of tenant and end user, erasable per person.

how it is kept
  • A Skill's description sits in the prompt and its body loads only when the model asks, so a long policy is not billed every turn.
  • end_user_id is required alongside memory: a default would point every end user at one bucket of facts.
  • [[skill:name]] links in instructions are validated at publish.
  • Retrieval, embeddings and vector stores are refused. You already have one.
DESIGN.md §15, §16 · guide
04

Other agents over A2A

A Run can be exposed as an A2A Task, and a remote agent can be called as a tool source.

how it is kept
  • The protocol and the mapping ship; the server does not, for the same reason nothing else here brings one.
  • Agent Cards, JSON-RPC and REST envelopes, version and extension negotiation, JWS card signing, push notifications.
  • A peer pool is keyed by (scope, peer, credential) for the same reason the MCP pool is.
docs/design-notes · guide

Control

Who may do what, what stops before it happens, and what fits in the prompt.

01

Tool access only narrows

What the server offers contains what the tenant permits, contains what the Spec grants, contains what is callable now.

how it is kept
  • One function computes that intersection and both the validator and the runtime call it, so the two cannot drift.
  • A Scope threads through every call and is stamped on every Record. Every read path takes scope= and refuses another tenant's Run.
  • MCP clients pool by (scope, server, credential) and never by URL, which is the one line that would otherwise send one tenant's token on another's call.
  • Every outbound HTTP call, the model client included, goes through one egress seam with an EgressPolicy.
DESIGN.md §10.4, §10.5, §14 · guide
02

Approval before selected tool calls

A Run suspends before any tool your selectors mark for approval, releases its lease and persists.

how it is kept
  • The process that approves need not be the one that asked, and the decision and who made it are both in the log.
  • Tools carry MCP-style annotations: read-only, write, destructive. An unannotated tool is treated as write, never exempt.
  • status().pending_approval names the exact call waiting. resume(approved=True, by=...) records who decided.
  • Suspension has four reasons: approval, question, external, children. One mechanism, not four.
DESIGN.md §10.9, §11 · guide
03

Stop or steer a running agent

An interrupt during a tool call stops the Run, and a message sent immediately after starts a new one carrying it.

how it is kept
  • Both are visible in the log, in order, so a stop and the message racing it cannot be misread as either alone.
  • An interruptible call is cancelled; a non-interruptible one finishes, so a refund is never half-issued.
  • Three queues: steer this turn, follow up after it, or hand a message to the next Run.
  • stream(after=N) resumes across the interrupt with no gap, because the log is the stream.
DESIGN.md §9, §12, §23.3, §23.4 · guide
04

Long conversations and large results

History is summarised past a token threshold measured from real provider usage, leaving the log untouched.

how it is kept
  • Oversized tool results are offloaded to a BlobStore and handed to the model as a readable handle.
  • A CompactionApplied Record replaces a range in the model's view, not in the log.
  • Elision (a Spec field) and offload (a Runtime field) are two thresholds on purpose, because they answer different questions.
DESIGN.md §10.8, §19 · guide

Observability

What it cost, what it is doing now, and how you test any of it.

01

Usage per call, cost only when priced

Usage is recorded per model call and split by cache state, and priced from a table you supply.

how it is kept
  • A model with no known price records cost=None, never 0, because a silent zero makes metering look correct and be wrong.
  • Input, output, cache read, cache write and reasoning tokens are separate fields, because they are billed at separate rates.
  • A Run priced in two currencies raises inconsistent_cost rather than summing them.
  • Latency splits wall-clock time into model, tool, queue and suspended time, and accounts for all of it.
DESIGN.md §13, §23.7 · guide
02

Stream and reconnect without gaps

A client reconnecting with after=N receives every later record and misses none, including across an interrupt.

how it is kept
  • The log is the stream, so there is no separate stream state that could get out of step with it.
  • stream() yields every Record and tails until the Run settles. stream_text() projects only the assistant's words for a chat UI.
  • An optional Notifier accelerates delivery; correctness never depends on it.
DESIGN.md §12, §23.4 · guide
03

OpenTelemetry spans for every step

Every Run, Attempt, Turn, model call, tool call and compaction opens a span under gen_ai.* conventions.

how it is kept
  • The adapter imports the OpenTelemetry API only, and never an exporter: which one you ship is a deployment's decision.
  • The span schema is declared in one place and conformance tests stop it drifting.
  • pip install psych-runtime[otel] pulls the API and the SDK. The SDK is what a consumer configures; the library itself only calls the API.
  • No collector, no dashboard. The playground wires Jaeger to show what a consumer's pipeline sees.
DESIGN.md §13.5 · guide
04

Tests without a network

Models, HTTP, MCP and A2A reach the network through injectable seams, and every test supplies its own.

how it is kept
  • No test calls a model provider or any other outside service. Store tests do connect, to database containers CI starts itself.
  • A scriptable fake model plays multi-step tool calling, malformed calls, stalled streams and streams that abort mid-token. FakeModel, LogBuilder and McpStubServer are exported for your own suite.
  • Every feature ships with an end-to-end case that fails when it breaks. A green unit suite over a broken e2e case is a broken build.
DESIGN.md §22 · guide