Psych Runtime

Project Overview
- Name: Psych Runtime
- Type: Open-source Python library - an embeddable agent runtime
- License: Apache-2.0, Python 3.12+
- Purpose: Give a company everything it needs to run AI agents inside its own product - the loop, durable execution, tool resolution, MCP, token accounting, a run report - without becoming a platform itself
- Links: psychruntime.com, docs at
/docs, a browser playground at/playground, published on PyPI aspsych-runtime
Overview
Psych Runtime is a library, not a platform: a company imports it to build their own agentic product and supplies the model, the tools, and the database. The one-line framing is "everything a platform needs except the platform" - Psych deliberately refuses to own an HTTP server, auth, orgs/roles, a database of its own, a scheduler, a UI, a prompt library, an eval framework, a vector store, budget enforcement, or a model router, because each of those is something the consumer already has, and owning it would turn a library into a framework.
psych_runtime.dispatch() admits a Run, a Worker claims and executes it, and psych_runtime.report() says what it did. Every state a Run reaches is a pure fold over an append-only, gapless Record log - a Worker that dies mid-tool-call is recovered by replaying that log, not by reconciling a snapshot against reality.
Design principles
- The log is the only truth. A Run's state is a pure fold over its Record log; a contradictory log raises a typed corruption error and fails loudly rather than being silently repaired.
- A lease and a deadline answer different questions. The lease follows process liveness and lets another Worker take over a dead one; a wall-clock deadline enforced in the process holding the Attempt catches a hang the lease cannot see.
- Access narrows and never widens. What the server offers contains what the tenant permits, contains what the Spec grants, contains what is callable now - one function computes that intersection so the validator and the runtime cannot drift apart.
- MCP clients pool by
(scope, server, credential), never by URL - pooling by URL alone eventually sends one tenant's OAuth token on another tenant's call. - Validation runs at publish, never at run. A Spec is data that hashes to an immutable Version; a customer waiting on a live response is the wrong place to discover a typo.
- An unknown model price records
cost=None, never0. A silent zero makes metering look correct while being wrong.
Key Capabilities
Agent and workflow specs
- Specs, never callables. An
AgentSpecreferences tools by name; a builder may take a Python function and register it, but what lands in the Spec is the registered name, so a Spec stays fully serialisable and content-hashable into an immutableVersion. - Workflows sequence deterministic steps (tool, agent, nested workflow) with step-name memoisation, so a crash resume does not re-execute completed steps.
- Subagents, two ways: a
SubagentRefroster an author writes, pinned into the parent's Version hash for crash recovery, and aSpawnEnvelopepermission that lets the model compose a child at run time, published as its own Version.
Runtime
- Durable execution - a Run survives its Worker being killed mid-tool-call; a second Worker reclaims the expired lease, replays the log, settles the dangling call, and completes.
- Interrupts and steering - stopping a Run and immediately sending a follow-up message is a modelled transition, not a race, across three queue kinds (steer, follow-up, next-run).
- Suspend / resume - a Run can pause for human approval, a clarifying question, an external webhook or clock, or background subagents, release its lease, and resume in a different process.
- Streaming - the log is the stream; a client reconnects with
after=Nand receives every later record, including across an interrupt, with no separate stream to fall out of sync.
Tools
- Three tool kinds - code (a registered Python function), HTTP (a templated REST call with credentials resolved through a
SecretResolverport, never a literal header in the Spec), and MCP. - MCP with tenant isolation - connections pool by
(scope, server, credential), large catalogues can defer disclosure out of the prompt, and OAuth 2.1 (client-credentials or authorization-code, PKCE, dynamic registration, scope step-up) authorizes a connection without Psych running a browser or a callback listener. - Approvals - MCP-style read-only/write/destructive annotations gate a tool behind a
Policyport; an unannotated tool is treated as destructive-adjacent (write) rather than exempt, closing the loophole where forgetting to annotate a tool would exempt it. - A failure-streak guard counts consecutive failures of one tool across an entire Run (not reset per turn) so a model cannot launder a broken tool by taking a turn off.
- Oversized results - independent elision (Spec-level, what the model sees inline) and offload (Runtime-level, where the bytes live) thresholds, backed by an in-memory, filesystem, or S3
BlobStore.
Observability, cost, and testing
psych_runtime.report()gives token totals split by cache state, cost, and a full latency breakdown;psych_runtime.answer(),status(),state(), andthread()are the other four read projections over the same log.- OpenTelemetry tracing over
gen_ai.*semantic conventions with a declared, conformance-tested span schema - Psych takes the OTel API and SDK and never chooses an exporter. - A scriptable
FakeModeldrives multi-step tool-calling runs including malformed tool calls, stalled streams, and streams that abort mid-token, so the whole test suite runs with no network call. - Four store adapters behind one port - in-memory, PostgreSQL, MySQL, and DynamoDB - proven identical by one contract suite rather than four separate code paths.
The on-ramp, kept honest
psych_runtime.session()fits the smallest working program on one screen, and thepsychCLI generates amain.pythe developer owns and runs themselves - the CLI never executes a Run itself, because a command line that can start a Worker is an operational surface Psych would have to keep working and versioning forever.- Every code example in the README, the docs, and every one of the 26
.claudeskills is executed by the end-to-end test suite, so a worked example that does not run is caught before a consumer copies it.
Architecture (high level)
core- Spec models, Version content-hashing, Record types, the pure reducer, corruption errors, and the status/conversation/answer projections. Imports nothing from the other packages; dependency direction is enforced mechanically with import-linter.runtime- the Worker, supervisor, Attempt, lease, agent loop, workflow engine, suspend/resume, interrupts, and steering queues. Supervision (deadline enforcement) and execution run as separate loops, because a loop that both executes an Attempt and enforces its own deadline cannot notice itself hanging.tools- registry, resolver, the code/HTTP/MCP executors, access narrowing, the failure-streak guard, large-result elision, and deferred MCP disclosure.model- theModelClientport, an OpenAI-compatible adapter, usage and pricing, prompt assembly, a transient-failure classifier, and the fake model used in tests.store- theStoreport plus memory, Postgres, MySQL, and DynamoDB adapters and their shared contract suite.sandbox,memory,telemetry,report,builder,testing- one port and its adapters each, all optional to a consumer that does not need them.
Every outbound HTTP call in the whole library, including the model client and MCP, goes through one egress seam - a control that covered three routes out of four would be worse than none, because someone would trust it.
Definition of done (v1), verified by the e2e suite
- The same agent built in Python and from a dict produce the same Version hash and run identically.
- A Run survives its Worker being killed mid-tool-call and is reclaimed by a second Worker.
- An interrupt stops a Run cleanly, and a message sent immediately after starts a new Run carrying it, both visible in the log in order.
- A reconnecting client with
after=Nreceives every later record, including across an interrupt. - A workflow with a nested agent resumes from its last completed step after a crash without re-executing completed steps.
- An MCP server connected mid-Run is usable next turn with no restart, and invisible to another tenant's Run.
report()gives correct token totals split by cache state, correct cost, and a latency breakdown across the Run's wall clock.- The same Spec runs identically on all four store adapters.
- A model program runs in the subprocess sandbox, calls a host tool, and its traceback on failure reaches the model as data.
- Three consecutive failures of one tool stop the model repeating it.
Technical Stack
- Language: Python 3.12+, fully typed (
mypy --strict, noAnyas an escape hatch) - Validation: Pydantic v2 models at every boundary - Records, Specs, tool arguments and results are validated models, never dicts
- Stores: in-memory, PostgreSQL (asyncpg), MySQL (aiomysql), DynamoDB (aioboto3), behind one port and one contract suite
- Sandbox: subprocess isolation by default, container isolation as a second backend - in-process sandboxing (
RestrictedPython, trimmed-builtinexec, AST filtering) is rejected outright as escapable - Telemetry: OpenTelemetry API/SDK over
gen_ai.*conventions - Docs site: a static Next.js export (psychruntime.com) generated from the library's own
__all__, docstrings, and design notes rather than hand-maintained, so a doc that drifts from the code fails the build - Tooling:
uv,ruff,pytestacross unit, functional, and end-to-end layers, all blocking in CI as separate jobs - Packaging: published to PyPI as
psych-runtime