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.
| Field | Type | Notes |
|---|---|---|
tenant | str | |
principal | `str | None` |
labels | dict |
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_forsatisfiespsych_runtime.tools.resolver.McpCatalog, so a turn's tool set includes what each granted server currently offers.callruns 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 <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.
| Member | Value |
|---|---|
READ_ONLY | read-only |
WRITE | write |
DESTRUCTIVE | destructive |
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.
| Field | Type | Notes |
|---|---|---|
identity | str | |
secret | SecretStr |
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.
| Field | Type | Notes |
|---|---|---|
cpu_seconds | float | |
address_space_bytes | int | |
file_size_bytes | int | |
process_count | int | |
wall_seconds | float |
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.
| Field | Type | Notes |
|---|---|---|
stdout | str | |
stderr | str | |
value | Any | |
failure | `psych_runtime.sandbox.port.SandboxFailure | None` |
duration_seconds | float | |
limit_hit | `psych_runtime.sandbox.port.SandboxLimit | None` |
network_denied | bool | |
stdout_truncated | bool | |
stderr_truncated | bool | |
stdout_data | bytes | |
stderr_data | bytes | |
stdout_size | int | |
stderr_size | int | |
isolation | `code_execution.IsolationLevel | None` |
guarantees | SandboxGuarantees | |
artifacts | tuple | |
artifacts_omitted | int | |
cancelled | bool |
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.
| Field | Type | Notes |
|---|---|---|
kind | str | |
message | str | |
traceback | `str | None` |
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.
| Member | Value |
|---|---|
CPU_SECONDS | cpu_seconds |
ADDRESS_SPACE_BYTES | address_space_bytes |
FILE_SIZE_BYTES | file_size_bytes |
PROCESS_COUNT | process_count |
WALL_SECONDS | wall_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.
| Field | Type | Notes |
|---|---|---|
backend | str | |
platform | str | |
isolation | `code_execution.IsolationLevel | None` |
guarantees | SandboxGuarantees | |
mechanisms | tuple | |
network_grant_supported | bool | |
artifacts_supported | bool | |
languages | tuple | |
limit_ceiling | `psych_runtime.sandbox.port.SandboxLimits | None` |
ready | bool | |
problems | tuple | |
notes | tuple |
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.
| Field | Type | Notes |
|---|---|---|
filesystem | Enforcement | |
network | Enforcement | |
process_tree | Enforcement | |
identity | Enforcement | |
cpu | Enforcement | |
memory | Enforcement | |
file_size | Enforcement | |
process_count | Enforcement | |
wall_clock | Enforcement | |
environment | Enforcement |
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.
| Field | Type | Notes |
|---|---|---|
path | str | |
size_bytes | int | |
data | bytes | |
content_type | str | |
truncated | bool |
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.
| Field | Type | Notes |
|---|---|---|
stream_bytes | int | |
collect_artifacts | bool | |
artifact_count | int | |
artifact_bytes | int |
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.
| Member | Value |
|---|---|
ENFORCED | enforced |
UNVERIFIED | unverified |
UNAVAILABLE | unavailable |
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.
| Field | Type | Notes |
|---|---|---|
run_id | RunId | |
scope | Scope | |
version_hash | VersionHash | |
state | RunState | |
created_at | datetime | |
deadline_at | datetime | |
lease_holder | Optional | |
lease_expires_at | `datetime.datetime | None` |
idempotency_key | `str | None` |
attempt_count | int | |
runnable_at | `datetime.datetime | None` |
parent_run_id | Optional | |
delegation_depth | int | |
continues_run_id | Optional |
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.
| Member | Value |
|---|---|
RUNNABLE | runnable |
RUNNING | running |
SUSPENDED | suspended |
NESTED | nested |
SETTLED | settled |
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).
| Field | Type | Notes |
|---|---|---|
model | str | |
messages | tuple | |
tools | tuple | |
temperature | `float | None` |
top_p | `float | None` |
max_output_tokens | `int | None` |
reasoning_effort | Optional | |
stop | tuple | |
cache_breakpoints | tuple | |
idle_timeout_seconds | float | |
extra | dict |
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.
| Field | Type | Notes |
|---|---|---|
queue_wait_seconds | float | |
time_to_first_token_seconds | `float | None` |
stream_duration_seconds | float |
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.
| Field | Type | Notes |
|---|---|---|
name | str | |
description | str | |
input_schema | dict | |
annotations | frozenset |
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.
| Field | Type | Notes |
|---|---|---|
id | str | |
key | MemoryKey | |
content | str | |
created_at | datetime |
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.
| Field | Type | Notes |
|---|---|---|
tenant | str | |
end_user_id | str |
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.
| Field | Type | Notes |
|---|---|---|
input | Decimal | |
output | Decimal | |
cache_read | Decimal | |
cache_write | Decimal | |
cache_write_1h | `decimal.Decimal | None` |
currency | str |
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.