Psych Runtime

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 as psych-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, never 0. A silent zero makes metering look correct while being wrong.

Key Capabilities

Agent and workflow specs

  • Specs, never callables. An AgentSpec references 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 immutable Version.
  • 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 SubagentRef roster an author writes, pinned into the parent's Version hash for crash recovery, and a SpawnEnvelope permission 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=N and 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 SecretResolver port, 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 Policy port; 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(), and thread() 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 FakeModel drives 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 the psych CLI generates a main.py the 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 .claude skills 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 - the ModelClient port, an OpenAI-compatible adapter, usage and pricing, prompt assembly, a transient-failure classifier, and the fake model used in tests.
  • store - the Store port 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

  1. The same agent built in Python and from a dict produce the same Version hash and run identically.
  2. A Run survives its Worker being killed mid-tool-call and is reclaimed by a second Worker.
  3. 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.
  4. A reconnecting client with after=N receives every later record, including across an interrupt.
  5. A workflow with a nested agent resumes from its last completed step after a crash without re-executing completed steps.
  6. An MCP server connected mid-Run is usable next turn with no restart, and invisible to another tenant's Run.
  7. report() gives correct token totals split by cache state, correct cost, and a latency breakdown across the Run's wall clock.
  8. The same Spec runs identically on all four store adapters.
  9. A model program runs in the subprocess sandbox, calls a host tool, and its traceback on failure reaches the model as data.
  10. Three consecutive failures of one tool stop the model repeating it.

Technical Stack

  • Language: Python 3.12+, fully typed (mypy --strict, no Any as 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-builtin exec, 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, pytest across unit, functional, and end-to-end layers, all blocking in CI as separate jobs
  • Packaging: published to PyPI as psych-runtime

Architecture Diagram

Drag to pan, scroll to zoom