Psych Runtime
Reference

Errors

What is raised, and what each one means about the state you are in.

This is the documentation for unreleased changes on main. For the released library, read v0.1.

PsychError

exception

Root of everything Psych raises.

AccessDenied

exception

class AccessDenied(subject: 'str', reason: 'str') -> 'None'

The consumer's Policy port refused, or narrowing removed the tool.

Carries which, because "you may not" and "that no longer exists for you" are different problems for whoever is reading the log.

RunNotFound

exception

class RunNotFound(run_id: 'str') -> 'None'

Something went wrong at the Store port.

RunNotSuspended

exception

class RunNotSuspended(run_id: 'str', state: 'str') -> 'None'

resume() was called on a Run that is not waiting for anything.

Typed rather than a bare ValueError so an HTTP layer can map it to a status without matching on message text.

RunAlreadySettled

exception

class RunAlreadySettled(run_id: 'str', what: 'str') -> 'None'

A Run that has reached its terminal record was asked to do more.

Raised by send(); interrupt() on a settled Run is a no-op by contract, since the first abort is the one that counts.

RunEndedWithoutAnswer

exception

A Run a caller was reading ended without producing one.

Raised only by psych_runtime.stream_text(), which is a projection that yields words and therefore has no way to show an ending that produced none. Returning quietly would leave a UI with a blank reply and no error for a Run whose log says exactly what went wrong, so this is the one place a projection raises rather than renders.

Every other read path returns the ending as data instead, because they can: status() names the lifecycle, answer() sets finished to False, and report() carries the terminal state and the failure.

RunAborted

exception

class RunAborted(run_id: 'str', state: 'str') -> 'None'

The Run stopped before answering: interrupted, or past its deadline.

state distinguishes them, which a caller usually needs: a user pressing stop is not an incident and a deadline is.

RunFailed

exception

class RunFailed(run_id: 'str', failure: 'object | None' = None) -> 'None'

The Run settled FAILED.

failure is the one recorded in the log, so a caller reports what happened rather than a generic message about streaming.

SuspensionExpired

exception

class SuspensionExpired(run_id: 'str', reason: 'str') -> 'None'

A suspended Run waited past its expiry and is settled as abandoned.

DESIGN.md §11: suspensions expire rather than waiting forever on a user who left.

SpecValidationError

exception

class SpecValidationError(issues: 'list[ValidationIssue]') -> 'None'

A Spec was refused at publish.

DESIGN.md §4: validation happens at publish, never at run. A customer waiting on a response is not the right place to discover a typo, so this is raised by publish() and never by the agent loop.

ValidationIssue

class

class ValidationIssue(path: 'str', message: 'str') -> 'None'

One problem with a Spec, naming exactly what is wrong and where.

Not an exception: a publish reports every issue at once rather than making the author fix one typo per attempt.

Attributes: path: where in the Spec the problem is, dotted from the root. message: what is wrong, in a sentence an author can act on.

StoreError

exception

Something went wrong at the Store port.

SeqConflict

exception

class SeqConflict(run_id: 'str', seq: 'int') -> 'None'

Appending Record seq found one already there.

The single-writer rule (DESIGN.md §6 rule 3) says exactly one Attempt holds the lease and may append, so this means another writer exists. The losing Attempt aborts immediately rather than retrying at the next sequence, which would interleave two Attempts' records into one log.

LeaseLost

exception

class LeaseLost(run_id: 'str', worker_id: 'str') -> 'None'

This Attempt no longer holds the lease it was working under.

Raised when a renewal is refused because another Worker reclaimed an expired lease. The Attempt stops immediately: anything it writes from here would be a second writer on a log that permits one.

DeadlineExceeded

exception

class DeadlineExceeded(run_id: 'str', deadline_seconds: 'float') -> 'None'

A Run passed the deadline every Run has (DESIGN.md §8.4).

TransientError

exception

class TransientError(message: 'str', *, retry_after_seconds: 'float | None' = None) -> 'None'

A failure the classifier judged worth retrying (DESIGN.md §8.6).

Retries draw on a per-Run budget rather than a per-call one, so a Run cannot retry forever by spreading failures across steps.

CorruptLog

exception

class CorruptLog(reason: 'CorruptionReason', run_id: 'str', seq: 'int | None', detail: 'str') -> 'None'

A Run's log is in a state the protocol cannot produce.

Raised by the reducer, never caught by the runtime to work around. The Run fails and the error names the reason, the sequence and the Run so the writer bug can be found from the log alone.

Attributes: reason: which invariant broke. run_id: whose log it is. seq: the record the fold was looking at when it noticed. Not necessarily the record that is wrong, because the contradiction is between records, but it is where to start reading. detail: what specifically contradicts what.

CorruptionReason

enum

class CorruptionReason(*values)

Why a log is impossible. Each member is a distinct writer bug.

MemberValue
MULTIPLE_OPEN_OPERATIONSmultiple_open_operations
UNKNOWN_OPERATIONunknown_operation
RECORD_AFTER_FINISHrecord_after_finish
NON_CONSECUTIVE_SEQnon_consecutive_seq
NON_CONSECUTIVE_ATTEMPTnon_consecutive_attempt
QUEUE_AFTER_ABORTqueue_after_abort
INVALID_QUEUE_CANCELLATIONinvalid_queue_cancellation
INCONSISTENT_STEPinconsistent_step
TOOL_CALL_MISMATCHtool_call_mismatch
DUPLICATE_TOOL_INVOCATIONduplicate_tool_invocation
PROVISIONED_ENTRY_MISMATCHprovisioned_entry_mismatch
INVALID_DEFERRED_HANDLEinvalid_deferred_handle
INVALID_COMPACTION_REASONinvalid_compaction_reason
INCONSISTENT_COSTinconsistent_cost

CredentialNotFound

exception

class CredentialNotFound(scope: 'Scope', name: 'str') -> 'None'

A Spec named a credential that has no value for this Scope.

Raised by a caller of SecretResolver.resolve (psych_runtime.tools.mcp among them) rather than by the port itself, so that whether a missing credential is fatal stays a decision the caller makes deliberately: Psych never falls back to treating "no credential configured" as "connect without one", which would be a much quieter failure than this.

BuilderError

exception

The builder was asked to produce a Spec it cannot construct.

On this page