Psych Runtime
Reference

Runtime

What executes a Run, and what you hand it.

Runtime

class

class Runtime(store: 'Store', model: 'ModelClient', registry: 'ToolRegistry', resolver: 'ToolResolver | None' = None, executor: 'ToolExecutor | None' = None, prices: 'PriceResolver | None' = None, cost_policy: 'CostPolicy' = 'prefer_provider', memories: 'Sequence[str]' = <factory>, policy: 'Policy | None' = None, approval_selectors: 'Sequence[str]' = <factory>, telemetry: 'Telemetry | None' = None, sandbox: 'Sandbox | None' = None, sandboxes: 'SandboxProfiles | Sequence[SandboxProfile] | None' = None, code_execution_policy: 'CodeExecutionPolicy | None' = None, memory: 'MemoryPort | None' = None, end_user_id: 'str | None' = None, mcp: 'McpTools | None' = None, a2a: 'A2ATools | None' = None, blob: 'BlobStore | None' = None, catalogue_budget_chars: 'int' = 20000, blob_offload_bytes: 'int' = 300000, http: 'HttpCaller | None' = None)

Everything a Worker needs to execute a Run.

Assembled once at boot by the consumer and handed to every Worker in the process. Holds no per-Run state, so one instance serves any number of concurrent attempts.

Worker

class

class Worker(store: 'Store', runner: 'AttemptRunner', *, worker_id: 'WorkerId | None' = None, lease_seconds: 'float' = 30.0, heartbeat_seconds: 'float' = 10.0, grace_seconds: 'float' = 60.0, supervisor_interval: 'float' = 5.0, poll_interval: 'float' = 0.5, concurrency: 'int' = 4, max_attempts: 'int' = 10) -> 'None'

Claims runnable Runs and executes them until told to stop.

Session

class

class Session(store: 'Store', registry: 'ToolRegistry', runtime: 'Runtime', worker: 'Worker', scope: 'Scope', _versions: 'dict[str, Version]' = <factory>)

A running Worker, a store, and the publish-dispatch-wait sequence.

Built by session(); constructing one directly means owning the Worker's lifecycle yourself, which is what session() exists to avoid.

The four fields below are the real objects, not wrappers. A consumer who needs a second Worker, a per-tenant Runtime or a store transaction reaches through them rather than around this class.

Dispatched

class

class Dispatched(run_id: 'RunId', created: 'bool')

What a dispatch produced.

Attributes: run_id: the Run, new or existing. created: False when an existing Run was returned for a repeated idempotency key. A caller that wants to know whether their retry was the one that did the work reads this.

ToolRegistry

class

class ToolRegistry() -> 'None'

Names to functions, for one process.

Not thread-safe for concurrent registration, and deliberately so: registration happens at boot on one thread, and a lock here would suggest otherwise. Lookups after boot are reads and are safe from anywhere.

Scope

model

class Scope(*, tenant: Annotated[str, MinLen(min_length=1), MaxLen(max_length=256)], principal: Annotated[str | None, MaxLen(max_length=256)] = None, labels: dict[str, str] = <factory>)

The tenancy and identity context threaded through every call.

Frozen, because a Scope that can be mutated after a Record is stamped with it is a Scope that can be mutated between the authorization check and the call it authorised.

Attributes: tenant: the isolation boundary. Two Runs with different tenants must never see each other's data, and this is the field that decides it. principal: who is acting, when the consumer knows. Psych records it and passes it to Policy; it never interprets it. labels: free-form consumer metadata, stamped on Records and available for their own filtering and metering.

FieldTypeNotes
tenantstr
principal`strNone`
labelsdict

McpPool

class

class McpPool(*, transport: 'McpTransport', secrets: 'SecretResolver', catalogue_ttl_seconds: 'float' = 300.0, oauth: 'OAuthClient | None' = None, oauth_identity: 'ClientIdentityConfig | None' = None, oauth_grant: 'GrantKind' = 'client_credentials') -> 'None'

Pools McpConnections by McpPoolKey.

One pool per process is the intended shape: every Run, for every tenant, shares it, and isolation comes entirely from the key, not from having a separate pool per tenant (which would just move the one-line mistake DESIGN.md §10.4 warns about to whoever wires up per-tenant pools).

oauth is optional. Absent, a server behind OAuth still fails with McpServerUnreachable on its first 401, the same as it always has, except the message now says the server wants OAuth rather than just naming the status code. Present, this pool's connections acquire, refresh, and step up tokens against it automatically; see this module's docstring for the full flow and how the pool key follows an acquired token.

oauth_identity and oauth_grant are the default client identity and grant kind, used only for a server whose Spec sets no McpServer.oauth. A server that does declare one is connected with that identity and grant instead, so one pool serves any number of servers with different OAuth client identities and different grant kinds in the same Run -- see this module's docstring, "One pool, many OAuth identities and grants".

McpTools

class

class McpTools(pool: 'McpPool', *, tenant_policy: 'Any' = None, describe_server: 'Callable[[Scope, McpServer], str | None] | None' = None) -> 'None'

The seam between a pool of MCP connections and a running agent.

Two halves, because the resolver and the executor need different things and both must narrow the same way (DESIGN.md §10.5):

  • tools_for satisfies psych_runtime.tools.resolver.McpCatalog, so a turn's tool set includes what each granted server currently offers.
  • call runs one of those tools.

They live on one object rather than being wired separately because they share the inputs that decide access: the pool, which carries tenancy in its key, and the tenant policy. Wiring them from two places is how the resolver's view and the executor's view drift apart, and a drift in this direction is a model calling a tool the Spec excluded.

Why call narrows again

The resolver already filtered the tool set the model was shown. That is not a control, because the model chooses the name it sends: a caller that looked a name up across every connected server and invoked whatever answered would let a model reach a tool the Spec's allow list excluded simply by naming it. So this applies the same narrow the resolver applies, against the same three planes, and a name that does not survive it is refused however the model got hold of it.

Tenancy rides on scope: a name is resolved only against connections keyed to the calling Scope, so tenant A's Run cannot reach tenant B's server even if both grant a tool of the same name.

DEFAULT_MAX_SSE_EVENT_BYTES

value

The default process-wide maximum size of one MCP server-sent event, in bytes.

max_sse_event_bytes

function

def max_sse_event_bytes() -> 'int'

The ceiling currently applied to every new event stream.

set_max_sse_event_bytes

function

def set_max_sse_event_bytes(limit: 'int') -> 'None'

Set the process-wide ceiling. Applies to streams opened afterwards.

A2APool

class

class A2APool(*, transport: 'A2ATransport', secrets: 'SecretResolver', card_ttl_seconds: 'float' = 300.0) -> 'None'

Pools A2AConnections by A2APoolKey.

One pool per process, shared by every Run of every tenant, with isolation coming entirely from the key -- the same shape as McpPool, and for the same reason: a pool per tenant just moves the one-line mistake to whoever wires the pools up.

A2ATools

class

class A2ATools(pool: 'A2APool', *, tenant_policy: 'Any' = None) -> 'None'

The seam between a pool of peer connections and a running agent.

Two halves that must narrow identically, on one object for the reason McpTools gives: wiring the resolver's view and the executor's view from two places is how a model ends up able to call a peer the Spec excluded.

HttpTransport

class

class HttpTransport(*, policy: 'EgressPolicy | None' = None, client: 'httpx.AsyncClient | None' = None, timeout: 'httpx.Timeout | float | None' = Timeout(connect=10.0, read=30.0, write=30.0, pool=10.0)) -> 'None'

The one object that owns outbound HTTP in Psych.

Wraps a single httpx.AsyncClient and checks every request against an EgressPolicy before it goes out. The model client, HTTP tool executor and MCP client all take one of these rather than building their own httpx client, which is what makes the seam a single seam instead of a convention repeated at each call site.

Two methods only: request for a call that returns a complete response, and stream for one where the body is consumed incrementally, which the OpenAI-compatible model adapter needs for server-sent events. Both check the policy before doing anything else.

OpenAICompatibleClient

class

class OpenAICompatibleClient(*, base_url: 'str', transport: 'HttpTransport', scope: 'Scope', api_key: 'str | None' = None, headers: 'Mapping[str, str] | None' = None) -> 'None'

ModelClient over the OpenAI-compatible chat completions wire format.

Attributes are private; construct with keyword arguments only.

Args: base_url: the API root, e.g. https://proxy.internal/v1. Requests go to \{base_url}/chat/completions and \{base_url}/models. transport: the egress seam. Never construct an httpx client here directly; that would be a second, uncontrolled path to the network. scope: the tenant this client instance acts for. Stamped on every egress check this client makes. api_key: sent as Authorization: Bearer &lt;api_key> when set. Some proxy deployments authenticate at the network layer instead and need none. headers: extra headers merged into every request, for a proxy that wants a tenant header or a routing hint.

AllowAll

class

class AllowAll()

The default Policy: yes to everything.

Psych ships this because a consumer evaluating the library should not have to write an authorization system first, and because Psych genuinely does not have an opinion about who may do what. It is not a security control and its name says so.

Decision

class

class Decision(allowed: 'bool', reason: 'str' = '', requires_approval: 'bool' = False)

The consumer's answer.

Attributes: allowed: whether the call may proceed. reason: why not, when it may not. Reaches the model as the tool's result so it can explain to the user or try something else, which is why it should read as an explanation rather than an error code. requires_approval: the consumer wants a human decision. The Run suspends rather than failing, and resumes on the decision.

ToolClass

enum

class ToolClass(*values)

How dangerous a tool is, from its MCP annotations.

MemberValue
READ_ONLYread-only
WRITEwrite
DESTRUCTIVEdestructive

ResolvedCredential

model

class ResolvedCredential(*, identity: str, secret: pydantic.types.SecretStr)

One resolved credential: a secret value plus its non-secret identity.

identity is safe wherever secret is not: as a pool key, in a log line, in a repr taken while debugging. Pydantic's SecretStr already keeps secret out of this model's default repr and str (it prints as SecretStr('**********')), and the model is frozen so neither field can be swapped for an unmasked one after construction.

FieldTypeNotes
identitystr
secretSecretStr

SandboxLimits

model

class SandboxLimits(*, cpu_seconds: Annotated[float, Gt(gt=0)], address_space_bytes: Annotated[int, Gt(gt=0)], file_size_bytes: Annotated[int, Gt(gt=0)], process_count: Annotated[int, Gt(gt=0)], wall_seconds: Annotated[float, Gt(gt=0)])

Resource caps for one execution.

Defaults and rationale live on the adapters that apply them (psych_runtime.sandbox.subprocess, psych_runtime.sandbox.container), because a number with no story behind it is a number nobody can safely change later. This model only fixes the shape every adapter shares, so the contract suite (psych_runtime.sandbox.contract) can drive them with the same values and prove they mean the same thing on every backend.

Attributes: cpu_seconds: CPU time actually consumed, not wall-clock time. A program blocked waiting on a host binding's reply burns no CPU budget while it waits. address_space_bytes: the memory the process may use. A process backend applies it as the virtual address space (exceeding it surfaces to the program as an ordinary MemoryError, because CPython's allocator checks malloc's return value rather than trusting it); a container applies it as a cgroup memory limit; a Windows job object applies it as a per-process commit limit. file_size_bytes: the largest file the process may write, counting every file it opens, not a total across files. process_count: how many processes (Linux counts threads too) the execution may add at once, including its root process. See psych_runtime.sandbox.subprocess for why the kernel mechanism is a per-user limit on a process backend and what that implies for concurrent runs. wall_seconds: real time from spawn to teardown. This is the only cap here that is not a resource limit on the process; it is enforced by the host killing the process (or container) directly.

FieldTypeNotes
cpu_secondsfloat
address_space_bytesint
file_size_bytesint
process_countint
wall_secondsfloat

SandboxResult

model

class SandboxResult(*, stdout: str = '', stderr: str = '', value: Any = None, failure: psych_runtime.sandbox.port.SandboxFailure | None = None, duration_seconds: Annotated[float, Ge(ge=0)], limit_hit: psych_runtime.sandbox.port.SandboxLimit | None = None, network_denied: bool = False, stdout_truncated: bool = False, stderr_truncated: bool = False, stdout_data: bytes = b'', stderr_data: bytes = b'', stdout_size: Annotated[int, Ge(ge=0)] = 0, stderr_size: Annotated[int, Ge(ge=0)] = 0, isolation: psych_runtime.core.code_execution.IsolationLevel | None = None, guarantees: psych_runtime.sandbox.port.SandboxGuarantees = <factory>, artifacts: tuple[psych_runtime.sandbox.port.SandboxArtifact, ...] = (), artifacts_omitted: Annotated[int, Ge(ge=0)] = 0, cancelled: bool = False)

What one execution produced, success or failure.

Attributes: stdout: everything the program printed to standard output, decoded with replacement for the model's benefit. stdout_data is the same capture as raw bytes. stderr: everything it printed to standard error. value: what the program's top-level code evaluated to, JSON-shaped. None both when the program returned nothing and when it genuinely returned None; the two are not distinguished because JSON does not distinguish them either. failure: set when the program did not complete cleanly. See SandboxFailure. duration_seconds: wall-clock time from spawn to teardown. limit_hit: which cap in SandboxLimits ended the execution, if one did. None on a clean completion or on a plain program error that no limit caused. network_denied: True only when this execution actively verified that it has no route to the network, False when it did not (network was granted, or denial was requested but the host could not establish it). The same fact as guarantees.network being ENFORCED, kept as a boolean for the callers that predate the fuller report. stdout_truncated: the program printed more than the capture cap. stderr_truncated: same, for standard error. stdout_data: the captured standard output, raw. Empty when a backend predates raw capture; stdout is then the only copy. stderr_data: same, for standard error. stdout_size: how many bytes the program actually wrote, which exceeds len(stdout_data) when the capture was truncated. stderr_size: same, for standard error. isolation: the level this execution achieved, from achieved_level over guarantees. None when the backend could not grade itself or predates the report. guarantees: how each guarantee held on this execution. artifacts: regular files the program wrote, when collection was on. artifacts_omitted: how many files were left behind past the count cap. cancelled: the caller's cancellation ended this execution.

FieldTypeNotes
stdoutstr
stderrstr
valueAny
failure`psych_runtime.sandbox.port.SandboxFailureNone`
duration_secondsfloat
limit_hit`psych_runtime.sandbox.port.SandboxLimitNone`
network_deniedbool
stdout_truncatedbool
stderr_truncatedbool
stdout_databytes
stderr_databytes
stdout_sizeint
stderr_sizeint
isolation`code_execution.IsolationLevelNone`
guaranteesSandboxGuarantees
artifactstuple
artifacts_omittedint
cancelledbool

SandboxFailure

model

class SandboxFailure(*, kind: Annotated[str, MinLen(min_length=1), MaxLen(max_length=128)], message: Annotated[str, MaxLen(max_length=8192)], traceback: Annotated[str | None, MaxLen(max_length=65536)] = None)

A sandboxed program's failure, as data the model can read.

Deliberately shaped like psych_runtime.core.records.ToolFailure (kind, message, traceback) so a caller can build one from the other with a field-for-field copy. It is not the same type: psych_runtime.sandbox does not import psych_runtime.core.records for it, because a port should not depend on the record shape of whatever happens to consume it today, and tying the two together would make an unrelated change to the Record schema a breaking change here too.

FieldTypeNotes
kindstr
messagestr
traceback`strNone`

SandboxLimit

enum

class SandboxLimit(*values)

Which resource cap ended an execution, when one did.

A single field naming the failing dimension rather than five booleans: exactly one limit ends a given execution (the process dies, or the program raises, on the first cap it crosses), so a scalar is the honest shape and a set would imply a possibility that cannot happen.

MemberValue
CPU_SECONDScpu_seconds
ADDRESS_SPACE_BYTESaddress_space_bytes
FILE_SIZE_BYTESfile_size_bytes
PROCESS_COUNTprocess_count
WALL_SECONDSwall_seconds

SandboxProfile

class

class SandboxProfile(name: 'str', sandbox: 'Sandbox', hard_limits: 'SandboxLimits' = <factory>, allow_network: 'bool' = False, binding_budget: 'BindingBudget' = BindingBudget(max_calls=1000, max_calls_per_run=10000, max_argument_bytes=1048576, max_result_bytes=8388608, max_total_bytes=67108864, max_produced_bytes=268435456, max_preserved_bytes_per_run=536870912), description: 'SandboxDescription | None' = None)

One logical sandbox a deployment offers, by name.

Attributes: name: what a Spec's code_execution.profile names. sandbox: the implementation. Any object satisfying Sandbox, including one written against the earlier port. hard_limits: the ceiling no agent using this profile can exceed. allow_network: whether an agent may be granted raw network access through this profile. Off by default; on is a deliberate choice because it bypasses the egress seam for that program. binding_budget: how many host tool calls a program running here may make and how many bytes those calls may move. The deployment's answer to "a program can compress model turns while still generating unbounded host work", and the only ceiling on it: a Spec cannot raise this, and nothing below can either. description: what sandbox.describe() last said, filled by SandboxProfiles.verify or on first use. None until then.

SandboxProfiles

class

class SandboxProfiles(profiles: 'Iterable[SandboxProfile]' = ()) -> 'None'

The deployment's profile registry: names to implementations.

SandboxDescription

model

class SandboxDescription(*, backend: Annotated[str, MinLen(min_length=1), MaxLen(max_length=64)], platform: Annotated[str, MinLen(min_length=1), MaxLen(max_length=64)], isolation: psych_runtime.core.code_execution.IsolationLevel | None, guarantees: psych_runtime.sandbox.port.SandboxGuarantees, mechanisms: tuple[str, ...] = (), network_grant_supported: bool = False, artifacts_supported: bool = False, languages: tuple[str, ...] = ('python',), limit_ceiling: psych_runtime.sandbox.port.SandboxLimits | None = None, ready: bool = True, problems: tuple[str, ...] = (), notes: tuple[str, ...] = ())

What a backend can promise, learned before any program runs.

Sandbox.describe() returns one. A deployment reads it at startup to refuse an agent whose code_execution.isolation this backend cannot meet, and a console reads it to show a person what "isolated" means on this host. Nothing here is a guess: a backend that has not probed a mechanism reports it UNVERIFIED, and ready=False with a reason beats a green light that fails on first use.

Attributes: backend: a short stable identifier: subprocess, windows-job, bubblewrap, container, remote, or custom. platform: where programs run, as sys.platform spells it, or what a remote provider reports. isolation: the strongest level this backend can deliver here, from achieved_level over guarantees. None when it cannot reach even PROCESS. guarantees: what each dimension is worth on this backend. mechanisms: the named controls in use, for a person reading a settings screen: rlimit, setsid, netns, job_object, cgroups, user_namespace, seatbelt, and so on. network_grant_supported: whether network=True can be honoured. artifacts_supported: whether workspace files can be collected. languages: what the backend can run. Python only, today. limit_ceiling: caps this backend will not exceed whatever it is asked, or None when it has none of its own. ready: whether an execution would be attempted right now. problems: why not, when ready is false, in plain words. notes: facts a person configuring this backend should know that fit no other field: a deprecated interface in use, a shared per-user limit, an image that must be pulled first.

FieldTypeNotes
backendstr
platformstr
isolation`code_execution.IsolationLevelNone`
guaranteesSandboxGuarantees
mechanismstuple
network_grant_supportedbool
artifacts_supportedbool
languagestuple
limit_ceiling`psych_runtime.sandbox.port.SandboxLimitsNone`
readybool
problemstuple
notestuple

SandboxGuarantees

model

class SandboxGuarantees(*, filesystem: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, network: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, process_tree: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, identity: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, cpu: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, memory: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, file_size: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, process_count: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, wall_clock: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>, environment: psych_runtime.core.code_execution.Enforcement = <Enforcement.UNAVAILABLE: 'unavailable'>)

How each guarantee held, on one execution or for one backend.

One Enforcement per dimension a consumer might rely on. Every backend fills this from what it actually did and, where the child can check itself, from what the child reported: the network probe, the filesystem canary and the identity check all run inside the execution, so ENFORCED there is an observation and not a promise.

Attributes: filesystem: the host's filesystem is hidden from the program. Verified by the child failing to read a canary file the host placed outside the workspace. network: the program has no route out. Verified by a routing probe inside the child that never sends a packet. process_tree: every process the program starts ends with the execution, by the host killing the group, job or container. identity: the program runs as an account distinct from the worker's. Reported from the child's own effective uid where there is one. cpu: CPU time is capped by the kernel (rlimit, cgroup quota or job). memory: memory is capped by the kernel. file_size: the size of any one file is capped. process_count: the number of processes is capped. wall_clock: the host ends the execution at the wall-clock cap. environment: the child's environment was built from the allowlist alone, never from this process's own environment.

FieldTypeNotes
filesystemEnforcement
networkEnforcement
process_treeEnforcement
identityEnforcement
cpuEnforcement
memoryEnforcement
file_sizeEnforcement
process_countEnforcement
wall_clockEnforcement
environmentEnforcement

SandboxArtifact

model

class SandboxArtifact(*, path: Annotated[str, MinLen(min_length=1), MaxLen(max_length=512)], size_bytes: Annotated[int, Ge(ge=0)], data: bytes, content_type: str = 'application/octet-stream', truncated: bool = False)

One regular file the program wrote to its workspace.

Attributes: path: relative to the workspace, /-separated, never a host path. size_bytes: the file's size on disk. data: the bytes collected, up to the artifact budget. Shorter than size_bytes when truncated. content_type: guessed from the name, application/octet-stream when nothing better is known. truncated: the artifact budget cut this file short.

FieldTypeNotes
pathstr
size_bytesint
databytes
content_typestr
truncatedbool

OutputCapture

model

class OutputCapture(*, stream_bytes: Annotated[int, Ge(ge=4096)] = 1048576, collect_artifacts: bool = False, artifact_count: Annotated[int, Ge(ge=0)] = 16, artifact_bytes: Annotated[int, Ge(ge=0)] = 16777216)

How much of a program's output and workspace a backend keeps.

The bound on this process's memory, distinct from the caps in SandboxLimits that bound the child: a program may print a gigabyte and the host must not hold it. Every backend reads each stream until this many bytes, then discards the rest while still counting it, so SandboxResult.stdout_size reports what the program actually produced and stdout_truncated says the capture stopped short of it.

Attributes: stream_bytes: the most bytes captured per stream (stdout, stderr). collect_artifacts: whether regular files written under the workspace are read back into SandboxResult.artifacts. artifact_count: the most files collected. artifact_bytes: the most bytes collected across every file.

FieldTypeNotes
stream_bytesint
collect_artifactsbool
artifact_countint
artifact_bytesint

Enforcement

enum

class Enforcement(*values)

How much one guarantee is worth, on one execution or one backend.

A single word per guarantee rather than a boolean, because "we tried" and "we know it held" are different facts and a consumer relying on one of them needs to know which they have.

MemberValue
ENFORCEDenforced
UNVERIFIEDunverified
UNAVAILABLEunavailable

CodeExecutionGrant

class

class CodeExecutionGrant(limits: 'SandboxLimits', network: 'bool' = False, bindings: 'frozenset[str] | None' = None, isolation: 'IsolationLevel' = <IsolationLevel.PROCESS: 'process'>, artifacts: 'bool' = True, budget: 'BindingBudget' = BindingBudget(max_calls=1000, max_calls_per_run=10000, max_argument_bytes=1048576, max_result_bytes=8388608, max_total_bytes=67108864, max_produced_bytes=268435456, max_preserved_bytes_per_run=536870912))

What one party allows an execution: limits, network, bindings, level.

The unit intersect_grants operates on. A CodeExecutionPolicy returns one of these for a Scope, and the runtime intersects it with the grant it derived from the profile and the Spec, so a policy can only take away.

Attributes: limits: resource caps. network: whether raw network access may be granted at all. bindings: which tool names a program may call. None means "no opinion": every tool this Run may call, whatever its origin, resolved fresh at each turn boundary (psych_runtime.tools.bindings.effective_bindings). A frozenset restricts to those names and can never introduce one. isolation: the minimum level required. Intersecting takes the stronger requirement. artifacts: whether workspace files may be collected. budget: what a program may spend calling those tools.

BindingBudget

class

class BindingBudget(max_calls: 'int' = 1000, max_calls_per_run: 'int' = 10000, max_argument_bytes: 'int' = 1048576, max_result_bytes: 'int' = 8388608, max_total_bytes: 'int' = 67108864, max_produced_bytes: 'int' = 268435456, max_preserved_bytes_per_run: 'int' = 536870912)

What one program may spend calling the host's tools.

A program compresses model turns; it does not get to compress the host's work. One run_code call could otherwise issue a million MCP calls, or assemble a gigabyte of arguments, without the model paying a turn for any of it, so every binding call is counted and measured against this.

The deployment owns these numbers, not the Spec (psych_runtime.sandbox.profiles.SandboxProfile.binding_budget), and a CodeExecutionPolicy may narrow them per tenant. An agent's author is the wrong party to answer a denial-of-service question about the host they are running on, and a Spec field would put the answer inside the Version hash where changing it means republishing every agent.

Here in psych_runtime.core because both sides need it and neither may import the other: psych_runtime.sandbox carries it on a profile and psych_runtime.tools.bindings enforces it (DESIGN.md §21).

Attributes: max_calls: binding calls in one execution. max_calls_per_run: binding calls across every execution of one Run, so a model cannot buy a fresh allowance by writing a second program. max_argument_bytes: the serialised arguments of one call. max_result_bytes: the serialised result of one call, before it is sent back into the program. A larger result is refused rather than truncated: a program silently handed half a JSON document computes a wrong answer confidently. max_total_bytes: arguments plus results across one execution. This is transfer: bytes that actually crossed into the sandbox. max_produced_bytes: bytes the host's tools produced for one execution, whether or not the program received them. A result replaced by a handle was still fetched, parsed and held by the host, and a budget that counted only what crossed the boundary would price that at the size of the handle. max_preserved_bytes_per_run: bytes one Run's programs may leave stored in the log or the BlobStore. The ceiling that actually bounds somebody's storage bill, and the reason it is separate from max_produced_bytes: a result over the transfer ceiling is preserved before the program is told about it, so a refused transfer still costs storage, while a small inline result costs none. Per Run and counted from the log, so a Worker that dies does not hand the next Attempt a fresh allowance.

HostBinding

value

def HostBinding(*args, **kwargs)

No docstring. That is a bug in the library rather than in this page: the reader who found this symbol in their editor sees the same gap.

BlobKey

class

class BlobKey(tenant: 'str', run_id: 'RunId', call_id: 'ToolCallId')

A blob's address: tenant, Run and call, never fewer.

Deliberately not a bare string. See the module docstring's tenant-scoping section: the point of this type existing is that a caller cannot construct a valid address without a tenant, the same way Scope.pool_key makes tenancy a mandatory half of an MCP pool key rather than something a call site has to remember to include.

RunHeader

model

class RunHeader(*, run_id: psych_runtime.core.ids.RunId, scope: psych_runtime.core.scope.Scope, version_hash: psych_runtime.core.ids.VersionHash, state: psych_runtime.store.port.RunState, created_at: datetime.datetime, deadline_at: datetime.datetime, lease_holder: Optional[psych_runtime.core.ids.WorkerId] = None, lease_expires_at: datetime.datetime | None = None, idempotency_key: Annotated[str | None, MaxLen(max_length=256)] = None, attempt_count: Annotated[int, Ge(ge=0)] = 0, runnable_at: datetime.datetime | None = None, parent_run_id: Optional[psych_runtime.core.ids.RunId] = None, delegation_depth: Annotated[int, Ge(ge=0)] = 0, continues_run_id: Optional[psych_runtime.core.ids.RunId] = None)

The mutable row beside a Run's immutable log.

Everything that changes about a Run lives here; everything that happened lives in the log. Keeping the two apart is what lets the log stay append-only while a lease is renewed every few seconds.

Attributes: run_id: the Run. scope: tenancy. Every store query filters by it (DESIGN.md §14). version_hash: the Version this Run pinned at admission and reads for its whole life. state: claimability. lease_holder: the Worker holding the lease, if any. lease_expires_at: when that lease stops being honoured. A Run whose lease is past this is claimable by anyone, and that is the only mechanism by which a crashed Worker's Run recovers. deadline_at: when the supervisor should fire the abort signal. idempotency_key: what makes admission exactly-once per key. attempt_count: how many times this Run has been claimed. Bounds crash reclaim so a Run that kills every Worker it touches eventually stops. runnable_at: not claimable before this. Used by a Run waiting on a clock, which suspends as EXTERNAL. continues_run_id: the immediate predecessor Run in a conversation thread, mirrored from RunAdmitted.continues_run_id so a caller can check a continuation's Scope with one get_run rather than reading the whole log. Distinct from parent_run_id, which is delegation -- see that field's own docstring in psych_runtime.core.records.

FieldTypeNotes
run_idRunId
scopeScope
version_hashVersionHash
stateRunState
created_atdatetime
deadline_atdatetime
lease_holderOptional
lease_expires_at`datetime.datetimeNone`
idempotency_key`strNone`
attempt_countint
runnable_at`datetime.datetimeNone`
parent_run_idOptional
delegation_depthint
continues_run_idOptional

RunState

enum

class RunState(*values)

Where a Run is, from the store's point of view.

Deliberately coarser than the terminal states in psych_runtime.core.records: the store needs to know what is claimable, and the log holds the detail.

MemberValue
RUNNABLErunnable
RUNNINGrunning
SUSPENDEDsuspended
NESTEDnested
SETTLEDsettled

ModelRequest

model

class ModelRequest(*, model: Annotated[str, MinLen(min_length=1)], messages: tuple[typing.Annotated[psych_runtime.core.messages.SystemMessage | psych_runtime.core.messages.UserMessage | psych_runtime.core.messages.AssistantMessage | psych_runtime.core.messages.ToolResultMessage, FieldInfo(annotation=NoneType, required=True, discriminator='role')], ...], tools: tuple[psych_runtime.core.messages.ToolDefinition, ...] = (), temperature: float | None = None, top_p: float | None = None, max_output_tokens: int | None = None, reasoning_effort: Optional[Literal['low', 'medium', 'high']] = None, stop: tuple[str, ...] = (), cache_breakpoints: tuple[int, ...] = (), idle_timeout_seconds: Annotated[float, Ge(ge=0)] = 300.0, extra: dict[str, typing.Any] = <factory>)

One call to a model.

Attributes: model: the model id. A Spec names its model; there is no router (DESIGN.md §19). messages: the conversation, system message first. tools: what the model may call this turn. Resolved per turn and fixed for its duration, so the provider's cached prompt prefix stays valid (§10.2). cache_breakpoints: indices into messages after which the provider may place a cache breakpoint. Providers that do not support explicit breakpoints ignore this. Expressed as indices rather than as flags on the messages so that the same conversation can be sent to two providers with different caching models. idle_timeout_seconds: fail the read when no chunk arrives for this long. 0 disables it. The timer runs only while a source read is outstanding, so consumer backpressure never trips it (§8.5).

FieldTypeNotes
modelstr
messagestuple
toolstuple
temperature`floatNone`
top_p`floatNone`
max_output_tokens`intNone`
reasoning_effortOptional
stoptuple
cache_breakpointstuple
idle_timeout_secondsfloat
extradict

ModelTimings

model

class ModelTimings(*, queue_wait_seconds: Annotated[float, Ge(ge=0)] = 0.0, time_to_first_token_seconds: Annotated[float | None, Ge(ge=0)] = None, stream_duration_seconds: Annotated[float, Ge(ge=0)] = 0.0)

Where the time went in one model call (DESIGN.md §13.3).

Separate from the record's own timestamp because the gap between a Run's wall-clock and the sum of its parts is the interesting number, and it cannot be computed if the parts were never measured.

FieldTypeNotes
queue_wait_secondsfloat
time_to_first_token_seconds`floatNone`
stream_duration_secondsfloat

ToolDefinition

model

class ToolDefinition(*, name: Annotated[str, MinLen(min_length=1)], description: str = '', input_schema: dict[str, typing.Any] = <factory>, annotations: frozenset[str] = frozenset())

A tool as described to the model.

Built fresh at every turn by the resolver (DESIGN.md §10.2) from the Spec, the registry and whatever the MCP catalogue currently offers, then narrowed.

FieldTypeNotes
namestr
descriptionstr
input_schemadict
annotationsfrozenset

Memory

model

class Memory(*, id: Annotated[str, MinLen(min_length=1), MaxLen(max_length=64)], key: psych_runtime.memory.port.MemoryKey, content: Annotated[str, MinLen(min_length=1), MaxLen(max_length=4096)], created_at: datetime.datetime)

One durable fact, remembered for one end user.

FieldTypeNotes
idstr
keyMemoryKey
contentstr
created_atdatetime

MemoryKey

model

class MemoryKey(*, tenant: Annotated[str, MinLen(min_length=1), MaxLen(max_length=256)], end_user_id: Annotated[str, MinLen(min_length=1), MaxLen(max_length=256)])

The isolation boundary for durable memory.

Frozen and hashable so it can be a dict key in an adapter's own storage, which is exactly how psych_runtime.memory.store_backed uses it.

FieldTypeNotes
tenantstr
end_user_idstr

InMemoryStore

class

class InMemoryStore() -> 'None'

A Store backed by plain dicts, guarded by one asyncio.Lock.

Implements the Store protocol structurally; there is no base class to inherit because the port is a Protocol.

InMemoryBlobStore

class

class InMemoryBlobStore() -> 'None'

A BlobStore backed by a plain dict, guarded by one asyncio.Lock.

Implements the BlobStore protocol structurally; there is no base class to inherit because the port is a Protocol.

DEFAULT_PRICES

value

A PriceResolver over a fixed dict.

Exact match first, then the longest matching prefix. The prefix fallback exists because providers version model ids by suffix (gpt-4o-2024-08-06), and a table that only matched exactly would go stale silently every time a provider dated a release. A prefix match is still a real match against a rate someone entered, never a guess.

DEFAULT_PRICE_CATALOG_VERSION

value

The snapshot date of the bundled default price catalog, in YYYY-MM-DD format.

ModelPrice

model

class ModelPrice(*, input: Annotated[decimal.Decimal, Ge(ge=Decimal('0'))], output: Annotated[decimal.Decimal, Ge(ge=Decimal('0'))], cache_read: Annotated[decimal.Decimal, Ge(ge=Decimal('0'))], cache_write: Annotated[decimal.Decimal, Ge(ge=Decimal('0'))], cache_write_1h: Annotated[decimal.Decimal | None, Ge(ge=Decimal('0'))] = None, currency: Annotated[str, MinLen(min_length=3), MaxLen(max_length=3), _PydanticGeneralMetadata(pattern='^[A-Z]{3}$')] = 'USD')

Four per-million rates for one model.

Attributes: input: cost per million uncached input tokens. output: cost per million generated tokens. cache_read: cost per million tokens read from the prompt cache. cache_write: cost per million tokens written to the prompt cache at the provider's default retention. cache_write_1h: cost per million tokens written at one-hour retention. Some providers charge more for the longer hold. When None the cache_write rate applies to those tokens too. currency: ISO 4217, uppercase.

FieldTypeNotes
inputDecimal
outputDecimal
cache_readDecimal
cache_writeDecimal
cache_write_1h`decimal.DecimalNone`
currencystr

StaticPriceTable

class

class StaticPriceTable(prices: 'dict[str, ModelPrice]', *, overrides: 'dict[str, ModelPrice] | None' = None) -> 'None'

A PriceResolver over a fixed dict.

Exact match first, then the longest matching prefix. The prefix fallback exists because providers version model ids by suffix (gpt-4o-2024-08-06), and a table that only matched exactly would go stale silently every time a provider dated a release. A prefix match is still a real match against a rate someone entered, never a guess.

CostPolicy

value

def CostPolicy(*args, **kwargs)

No docstring. That is a bug in the library rather than in this page: the reader who found this symbol in their editor sees the same gap.

On this page