Reading a Run
Projections over the log. None is a second copy, so no two can disagree.
RunReport
model
class RunReport(*, run_id: psych_runtime.core.ids.RunId, scope: psych_runtime.core.scope.Scope, version_hash: psych_runtime.core.ids.VersionHash, spec_name: str, system_prompt: str, steps: tuple[psych_runtime.report.model.StepReport, ...], tool_calls: tuple[psych_runtime.report.model.ToolCallReport, ...], model_calls: tuple[psych_runtime.report.model.ModelCallReport, ...], suspensions: tuple[psych_runtime.report.model.SuspensionReport, ...], failure_streak_trips: tuple[psych_runtime.report.model.FailureStreakTrip, ...], terminal_state: psych_runtime.core.records.TerminalState | None, output: dict[str, typing.Any] | None, failure: psych_runtime.core.records.ToolFailure | None, orphaned_attempt_id: Optional[psych_runtime.core.ids.AttemptId], admitted_at: datetime.datetime, settled_at: datetime.datetime | None, totals: psych_runtime.report.model.TotalsReport, components: tuple[Component, ...] = (), subagents: tuple[psych_runtime.report.model.SubagentReport, ...] = (), subtree: psych_runtime.report.model.SubtreeTotals | None = None, tasks: tuple[psych_runtime.core.tasks.Task, ...] = (), compactions: tuple[psych_runtime.report.model.CompactionReport, ...] = (), continues_run_id: Optional[psych_runtime.core.ids.RunId] = None)The whole report for one Run. DESIGN.md §13.4.
Attributes:
version_hash: the Version this Run pinned at admission (DESIGN.md §4).
spec_name: the pinned Spec's name, for a report that reads on its own.
system_prompt: the system prompt as sent, read from the log. The most
recent turn's, which is the one a reader means by "the prompt";
every turn's own is on its ModelCallReport, because a turn
whose advisories changed was genuinely told something different.
This used to be reconstructed from the pinned Spec, which could not know what the runtime added at assembly time -- a withheld tool, an unreachable optional server, a deferred catalogue, what each connected system is for -- and so showed something the model never saw while looking authoritative. A Run recorded before the prompt was captured still falls back to that reconstruction, because there a guess beats an empty box.
Empty for a WorkflowSpec Run, which has no system prompt of its
own; a nested agent step's own prompt is on its child report.
steps: workflow steps, in the order each attempt started. Empty for a
bare agent Run.
tool_calls: every tool call, in the order each started.
model_calls: every model call, in the order each started.
suspensions: every suspend/resume cycle, in order.
failure_streak_trips: every time a tool's consecutive-failure count
crossed the guard's threshold, in order.
terminal_state: None while the Run has not settled yet.
orphaned_attempt_id: set only for a force_settled terminal state:
the Attempt the supervisor gave up waiting on (DESIGN.md §8.4). Its
dangling tool calls, if any, still show up in tool_calls with
outcome=None; this names whose work was abandoned.
totals: token, cost and latency totals over this Run alone. A child's
totals are not folded in; sum them yourself if that is what a caller
wants, since a workflow orchestrating several expensive children and
one cheap one is a different report from a flat sum either way.
continues_run_id: the Run this one continues, when it was admitted by
psych_runtime.dispatch(continues=...) as a later message in the same
conversation. None for a thread's first Run. Read
straight off RunAdmitted.continues_run_id -- the same field
psych_runtime.runtime.thread.load_thread_history walks to build the
conversation a Worker actually sent -- so this report and that
conversation can never disagree about which Runs make up the
thread. Totals above still cover this Run alone; an ancestor's own
cost is on its own report.
| Field | Type | Notes |
|---|---|---|
run_id | RunId | |
scope | Scope | |
version_hash | VersionHash | |
spec_name | str | |
system_prompt | str | |
steps | tuple | |
tool_calls | tuple | |
model_calls | tuple | |
suspensions | tuple | |
failure_streak_trips | tuple | |
terminal_state | `records.TerminalState | None` |
output | `dict[str, Any] | None` |
failure | `records.ToolFailure | None` |
orphaned_attempt_id | Optional | |
admitted_at | datetime | |
settled_at | `datetime.datetime | None` |
totals | TotalsReport | |
components | tuple | |
subagents | tuple | |
subtree | `psych_runtime.report.model.SubtreeTotals | None` |
tasks | tuple | |
compactions | tuple | |
continues_run_id | Optional |
RunStatus
model
class RunStatus(*, run_id: psych_runtime.core.ids.RunId, scope: psych_runtime.core.scope.Scope, version_hash: psych_runtime.core.ids.VersionHash, lifecycle: psych_runtime.core.status.Lifecycle, terminal_state: psych_runtime.core.records.TerminalState | None, output: dict[str, typing.Any] | None, failure_message: str | None, failure_kind: str | None, suspend_reason: psych_runtime.core.records.SuspendReason | None, suspend_expires_at: datetime.datetime | None, pending_approval: psych_runtime.core.status.PendingApproval | None, pending_question: psych_runtime.core.status.PendingQuestion | None, components: tuple[Component, ...], tasks: tuple[psych_runtime.core.tasks.Task, ...], turn: int, tool_calls: int, model_calls: int, head_seq: int, attempt_count: int, current_attempt: Optional[psych_runtime.core.ids.AttemptId], deadline_at: datetime.datetime | None, continues_run_id: Optional[psych_runtime.core.ids.RunId], parent_run_id: Optional[psych_runtime.core.ids.RunId], has_dangling_tool_calls: bool)One Run's current state, as a UI would render it.
| Field | Type | Notes |
|---|---|---|
run_id | RunId | |
scope | Scope | |
version_hash | VersionHash | |
lifecycle | Lifecycle | |
terminal_state | `records.TerminalState | None` |
output | `dict[str, Any] | None` |
failure_message | `str | None` |
failure_kind | `str | None` |
suspend_reason | `records.SuspendReason | None` |
suspend_expires_at | `datetime.datetime | None` |
pending_approval | `status.PendingApproval | None` |
pending_question | `status.PendingQuestion | None` |
components | tuple | |
tasks | tuple | |
turn | int | |
tool_calls | int | |
model_calls | int | |
head_seq | int | |
attempt_count | int | |
current_attempt | Optional | |
deadline_at | `datetime.datetime | None` |
continues_run_id | Optional | |
parent_run_id | Optional | |
has_dangling_tool_calls | bool |
RunStateView
class
class RunStateView(run_id: 'RunId', scope: 'Scope', version_hash: 'VersionHash', head_seq: 'int' = 0, admitted: 'bool' = False, settled: 'bool' = False, terminal_state: 'TerminalState | None' = None, output: 'dict[str, Any] | None' = None, failure: 'ToolFailure | None' = None, deadline_at: 'datetime | None' = None, parent_run_id: 'RunId | None' = None, delegation_depth: 'int' = 0, continues_run_id: 'RunId | None' = None, idempotency_key: 'str | None' = None, run_input: 'dict[str, Any]' = <factory>, attempt_count: 'int' = 0, current_attempt: 'AttemptId | None' = None, turn: 'int' = 0, turn_open: 'bool' = False, model_call_open: 'bool' = False, abort_seq: 'int | None' = None, suspended: 'bool' = False, suspend_reason: 'SuspendReason | None' = None, suspend_expires_at: 'datetime | None' = None, suspended_at: 'datetime | None' = None, pending_approval_call_id: 'ToolCallId | None' = None, suspend_question: 'str | None' = None, suspend_questions: 'tuple[AskedQuestion, ...]' = (), resumed_since_attempt: 'bool' = False, suspended_seconds: 'float' = 0.0, resume_payloads: 'dict[ToolCallId, dict[str, Any]]' = <factory>, approval_decisions: 'dict[ToolCallId, bool]' = <factory>, open_tool_calls: 'dict[ToolCallId, OpenToolCall]' = <factory>, tool_results: 'list[ToolResult]' = <factory>, steps: 'dict[StepId, StepRecord]' = <factory>, pending_steer: 'list[PendingQueueEntry]' = <factory>, pending_follow_up: 'list[PendingQueueEntry]' = <factory>, pending_next_run: 'list[PendingQueueEntry]' = <factory>, tasks: 'tuple[Task, ...]' = (), components: 'tuple[Component, ...]' = (), usage: 'Usage' = <factory>, cost: 'Cost | None' = None, unpriced_model_calls: 'int' = 0, provider_reported_costs: 'int' = 0, model_calls: 'int' = 0, failed_model_calls: 'int' = 0, transient_retries_used: 'int' = 0, last_prompt_tokens: 'int' = 0, failure_streaks: 'dict[str, int]' = <factory>, repeat_counts: 'dict[str, RepeatTally]' = <factory>, compaction_boundary_seq: 'int' = 0, compaction_summaries: 'list[str]' = <factory>, compaction_calls: 'int' = 0, children: 'dict[RunId, ChildRun]' = <factory>, result_handles: 'dict[str, ToolCallId]' = <factory>)Everything derivable from a Run's log.
Mutable during the fold and returned as-is: making it frozen would mean rebuilding it once per record, and the fold is the only thing that ever writes to it. Callers treat it as read-only.
AnswerView
model
class AnswerView(*, run_id: psych_runtime.core.ids.RunId, text: str = '', work: tuple[psych_runtime.core.answer.WorkTurn, ...] = (), finished: bool = False)A Run split into what it concluded and how it got there.
| Field | Type | Notes |
|---|---|---|
run_id | RunId | |
text | str | |
work | tuple | |
finished | bool |
ThreadView
model
class ThreadView(*, run_ids: tuple[psych_runtime.core.ids.RunId, ...], messages: tuple[psych_runtime.core.thread_view.MessageView, ...])One conversation, across every Run in its chain.
| Field | Type | Notes |
|---|---|---|
run_ids | tuple | |
messages | tuple |
MessageView
model
class MessageView(*, role: Literal['user', 'assistant', 'tool'], content: str, run_id: psych_runtime.core.ids.RunId, seq: int, at: datetime.datetime, tool_name: str | None = None, tool_call_id: Optional[psych_runtime.core.ids.ToolCallId] = None, is_error: bool = False)One message, and where in the log it came from.
| Field | Type | Notes |
|---|---|---|
role | Literal | |
content | str | |
run_id | RunId | |
seq | int | |
at | datetime | |
tool_name | `str | None` |
tool_call_id | Optional | |
is_error | bool |
WorkTurn
model
class WorkTurn(*, turn: int, text: str = '', tool_calls: tuple[psych_runtime.core.answer.ToolCallView, ...] = (), failure_message: str | None = None, at: datetime.datetime)One turn of working: what the model said on the way, and what it ran.
| Field | Type | Notes |
|---|---|---|
turn | int | |
text | str | |
tool_calls | tuple | |
failure_message | `str | None` |
at | datetime |
ToolCallView
model
class ToolCallView(*, call_id: psych_runtime.core.ids.ToolCallId, tool: str, arguments: dict[str, typing.Any], outcome: psych_runtime.core.records.ToolOutcome | None = None, result: Any = None, failure_message: str | None = None, duration_seconds: float | None = None, started_at: datetime.datetime, finished_at: datetime.datetime | None = None)One tool call, as a reader opening the work section sees it.
| Field | Type | Notes |
|---|---|---|
call_id | ToolCallId | |
tool | str | |
arguments | dict | |
outcome | `records.ToolOutcome | None` |
result | Any | |
failure_message | `str | None` |
duration_seconds | `float | None` |
started_at | datetime | |
finished_at | `datetime.datetime | None` |
TotalsReport
model
class TotalsReport(*, usage: psych_runtime.core.usage.Usage, cost: psych_runtime.core.usage.Cost | None, unpriced_model_calls: int, cost_is_incomplete: bool, provider_reported_costs: int = 0, model_calls: int, failed_model_calls: int, tool_calls: int, latency: psych_runtime.report.model.LatencyReport, compaction_calls: int = 0)Run-wide totals. DESIGN.md §13.4: "totals for tokens, cost and duration".
Attributes:
usage: every model call's usage summed with Usage.__add__, which
keeps cache_read and cache_write disjoint from input and
keeps cache_write_1h a subset of cache_write rather than a
second addend. Never re-derive this by hand from model_calls.
cost: the sum of every priced call's cost. None only when not one
call in the Run had a known price. Its source says where the
figures came from, and is "mixed" when the Run used both a
provider-reported cost and a locally computed one.
unpriced_model_calls: how many finished calls had no known price and so
are absent from cost. The one number that keeps "this run cost
$0.00" and "I could not price N of these calls" from looking the
same (DESIGN.md §13.2).
cost_is_incomplete: unpriced_model_calls > 0, spelled out as its own
field so a consumer does not have to remember which comparison
means "the total is honest but partial".
| Field | Type | Notes |
|---|---|---|
usage | Usage | |
cost | `usage.Cost | None` |
unpriced_model_calls | int | |
cost_is_incomplete | bool | |
provider_reported_costs | int | |
model_calls | int | |
failed_model_calls | int | |
tool_calls | int | |
latency | LatencyReport | |
compaction_calls | int |
LatencyReport
model
class LatencyReport(*, wall_clock_seconds: float, model_seconds: float, tool_seconds: float, compaction_seconds: float = 0.0, unaccounted_seconds: float)DESIGN.md §13.3: wall-clock, the sum of the measured parts, and the gap.
Attributes:
wall_clock_seconds: from the Run's first record to its last. Includes
everything, including a suspension's wait: the gap is meant to be
visible, not hidden because it happened to be long.
model_seconds: summed queue_wait_seconds + stream_duration_seconds
over every model call that finished. A failed or dangling call
carries no measured duration and contributes nothing here.
tool_seconds: summed duration_seconds over every tool call that
finished. A dangling call contributes nothing.
compaction_seconds: the same sum over every summarising call. Counted
apart from model_seconds for the reason
TotalsReport.compaction_calls is counted apart from
model_calls: it is time the Run spent fitting itself into the
window rather than doing the work, and a breakdown that blended the
two could not tell anyone whether compaction was worth it. It was
in neither before, so it landed in the gap below and read as time
nobody could explain.
unaccounted_seconds: wall_clock_seconds - model_seconds - tool_seconds - compaction_seconds, computed and reported rather
than silently dropped.
Ordinarily positive: time between Turns, suspension waits, and
anything else the runtime does not attribute to a model or a tool
all land here. Not clamped at zero, because a negative value is a
real signal too (clock skew between Workers, say) and clamping would
hide exactly what this field exists to show.
| Field | Type | Notes |
|---|---|---|
wall_clock_seconds | float | |
model_seconds | float | |
tool_seconds | float | |
compaction_seconds | float | |
unaccounted_seconds | float |
SubtreeTotals
model
class SubtreeTotals(*, usage: psych_runtime.core.usage.Usage, cost: psych_runtime.core.usage.Cost | None, unpriced_model_calls: int, cost_is_incomplete: bool, runs: int, complete: bool)One Run plus every descendant it spawned, added up.
RunReport.totals deliberately covers one Run alone and says so, because a
workflow orchestrating several expensive children and one cheap one is a
different report from a flat sum. This is the flat sum, kept beside it rather
than folded into it, so a reader picks the number they meant instead of
discovering which one they got.
Attributes:
complete: whether every descendant was actually counted. False when
a child was still running, or when child_depth stopped the walk
before the bottom of the tree. A total that could not say this would
look authoritative and be partial, which is the same failure
cost_is_incomplete exists to prevent one level down.
| Field | Type | Notes |
|---|---|---|
usage | Usage | |
cost | `usage.Cost | None` |
unpriced_model_calls | int | |
cost_is_incomplete | bool | |
runs | int | |
complete | bool |
SubagentReport
model
class SubagentReport(*, name: str, child_run_id: psych_runtime.core.ids.RunId, call_id: psych_runtime.core.ids.ToolCallId, child_version_hash: psych_runtime.core.ids.VersionHash, purpose: str, task: str, deliverable: str, tools: tuple[str, ...], model: str, delegation_depth: int, background: bool, spawned_at: datetime.datetime, finished_at: datetime.datetime | None, terminal_state: psych_runtime.core.records.TerminalState | None, output: dict[str, typing.Any] | None, failure: psych_runtime.core.records.ToolFailure | None, messages_sent: int, usage: psych_runtime.core.usage.Usage, cost: psych_runtime.core.usage.Cost | None, unpriced_model_calls: int, report: psych_runtime.report.model.RunReport | None = None, subtree: psych_runtime.report.model.SubtreeTotals | None = None)One subagent a Run composed and started (DESIGN.md §17).
The composed path only. A SubagentRef delegated to inline is a nested Run
reached through StepReport.child_run_id and stays there: the two are
different mechanisms with different lifetimes, and a report that merged them
would have to lie about one of them to describe the other.
Attributes:
child_version_hash: the Version the child pinned. The composed Spec is
published like any other, so a reader can fetch it and see exactly
the agent the model wrote.
tools: what the child actually held after narrowing, not what was asked
for.
terminal_state: None while it is still running -- or while its
ending has not reached this parent's log, which from the parent's
side is the same thing and is why the field is read from the log
rather than from the child.
report: the child's own report, present only when child_depth
allowed the walk. None does not mean there is nothing there.
subtree: this child and its own descendants, added up.
| Field | Type | Notes |
|---|---|---|
name | str | |
child_run_id | RunId | |
call_id | ToolCallId | |
child_version_hash | VersionHash | |
purpose | str | |
task | str | |
deliverable | str | |
tools | tuple | |
model | str | |
delegation_depth | int | |
background | bool | |
spawned_at | datetime | |
finished_at | `datetime.datetime | None` |
terminal_state | `records.TerminalState | None` |
output | `dict[str, Any] | None` |
failure | `records.ToolFailure | None` |
messages_sent | int | |
usage | Usage | |
cost | `usage.Cost | None` |
unpriced_model_calls | int | |
report | `psych_runtime.report.model.RunReport | None` |
subtree | `psych_runtime.report.model.SubtreeTotals | None` |
StepReport
model
class StepReport(*, step_id: psych_runtime.core.ids.StepId, name: str, kind: str, attempt_number: int, input: dict[str, typing.Any], completed: bool, output: dict[str, typing.Any] | None, failure: psych_runtime.core.records.ToolFailure | None, child_run_id: Optional[psych_runtime.core.ids.RunId], child: psych_runtime.report.model.RunReport | None = None)One workflow step: one checkpointed, memoised unit of work.
Populated from StepStarted/StepCompleted pairs, which only exist for
Steps a workflow wraps. A bare agent Run's Turns and tool calls are reported
through model_calls and tool_calls directly and never appear here,
because nothing wrapped them in a Step.
A step_id retried after a failure appears once per attempt, in the order each attempt started, so a report shows the whole story rather than only the attempt that stuck.
Attributes:
completed: False when the step started and the log ends before it
completed, the workflow analogue of a dangling tool call.
child_run_id: set when this step was a nested Run (DESIGN.md §5: a
workflow step may itself be an agent or a nested workflow). See
child for how it is walked.
child: the nested Run's own report, present only when the caller asked
build_report to include children and the depth bound allowed it.
None here does not mean there is no child; check
child_run_id for that.
| Field | Type | Notes |
|---|---|---|
step_id | StepId | |
name | str | |
kind | str | |
attempt_number | int | |
input | dict | |
completed | bool | |
output | `dict[str, Any] | None` |
failure | `records.ToolFailure | None` |
child_run_id | Optional | |
child | `psych_runtime.report.model.RunReport | None` |
ToolCallReport
model
class ToolCallReport(*, call_id: psych_runtime.core.ids.ToolCallId, tool: str, turn: int, step_id: Optional[psych_runtime.core.ids.StepId], arguments: dict[str, typing.Any], outcome: psych_runtime.core.records.ToolOutcome | None, result: Any, failure: psych_runtime.core.records.ToolFailure | None, duration_seconds: float | None, result_handle: str | None, preview: str | None, result_bytes: int, started_at: datetime.datetime, finished_at: datetime.datetime | None, interruptible: bool, safe_to_retry: bool)One tool call: what it was asked to do, and how it ended.
Attributes:
outcome: None only when the call is dangling: started, and the log
ends before a result was recorded (the same crash window the
reducer's has_dangling_tool_calls reports on the live Run).
result: the recorded result, whole. A large result was elided for the
model at call time (DESIGN.md §10.8); the log, and so this report,
always holds the full value.
duration_seconds: None for a dangling call, since none was measured.
| Field | Type | Notes |
|---|---|---|
call_id | ToolCallId | |
tool | str | |
turn | int | |
step_id | Optional | |
arguments | dict | |
outcome | `records.ToolOutcome | None` |
result | Any | |
failure | `records.ToolFailure | None` |
duration_seconds | `float | None` |
result_handle | `str | None` |
preview | `str | None` |
result_bytes | int | |
started_at | datetime | |
finished_at | `datetime.datetime | None` |
interruptible | bool | |
safe_to_retry | bool |
ModelCallReport
model
class ModelCallReport(*, turn: int, step_id: Optional[psych_runtime.core.ids.StepId], model: str, system_prompt: str, tool_names: tuple[str, ...], usage: psych_runtime.core.usage.Usage | None, cost: psych_runtime.core.usage.Cost | None, timings: psych_runtime.core.records.ModelTimings | None, finish_reason: str | None, text: str, tool_call_ids: tuple[psych_runtime.core.ids.ToolCallId, ...], started_at: datetime.datetime, finished_at: datetime.datetime | None, failure: psych_runtime.core.records.ToolFailure | None, will_retry: bool, dangling: bool)One model call: what was asked, what came back, what it cost.
Attributes:
turn: the Turn this call belongs to. Unique within a Run: DESIGN.md §5's
agent loop opens a fresh Turn for every model call, retries included,
so this is a stable key rather than a display counter.
step_id: set when the Turn ran inside a workflow step.
usage: None only when the call never finished, either because it
failed (see failure) or because the Attempt that made it died
mid-stream (see dangling). A finished call always carries usage,
even when its cost could not be computed.
cost: the cost recorded at call time, or None when the model had no
known price. Never a silent zero (DESIGN.md §13.2): read
RunReport.totals.cost_is_incomplete before summing these.
will_retry: whether the transient budget allowed another attempt after
this one failed. Only meaningful when failure is set.
dangling: the call started and the log ends before it settled. The
Attempt that made it died mid-call; a later Attempt does not retry
the model call itself, only the turn.
| Field | Type | Notes |
|---|---|---|
turn | int | |
step_id | Optional | |
model | str | |
system_prompt | str | |
tool_names | tuple | |
usage | `usage.Usage | None` |
cost | `usage.Cost | None` |
timings | `records.ModelTimings | None` |
finish_reason | `str | None` |
text | str | |
tool_call_ids | tuple | |
started_at | datetime | |
finished_at | `datetime.datetime | None` |
failure | `records.ToolFailure | None` |
will_retry | bool | |
dangling | bool |
SuspensionReport
model
class SuspensionReport(*, reason: psych_runtime.core.records.SuspendReason, question: str | None, pending_call_id: Optional[psych_runtime.core.ids.ToolCallId], suspended_at: datetime.datetime, expires_at: datetime.datetime, resumed_at: datetime.datetime | None, approved: bool | None, payload: dict[str, typing.Any] | None)One wait: why the Run paused, and how it ended.
Attributes:
resumed_at: None while still waiting, or when the log ends before a
resumed record arrived (an abandoned suspension past its expiry
settles the Run directly; see RunReport.terminal_state).
approved: set only for an approval suspension. False denies the
pending call, which DESIGN.md §11 treats as a settlement rather than
a failure.
| Field | Type | Notes |
|---|---|---|
reason | SuspendReason | |
question | `str | None` |
pending_call_id | Optional | |
suspended_at | datetime | |
expires_at | datetime | |
resumed_at | `datetime.datetime | None` |
approved | `bool | None` |
payload | `dict[str, Any] | None` |
CompactionReport
model
class CompactionReport(*, seq: int, reason: Literal['threshold', 'manual', 'overflow'], replaced_from_seq: int, replaced_to_seq: int, summary: str, model: str, usage: psych_runtime.core.usage.Usage, cost: psych_runtime.core.usage.Cost | None, at: datetime.datetime)One compaction: what it replaced, what it said, and what it cost.
The replaced Records are still in the log and still in this report's own
tool_calls and model_calls. Compaction changes what the model sees
next, never what happened, so this row says which stretch of the
conversation stopped being sent rather than which stretch stopped existing.
Attributes:
seq: where the compaction itself sits in the log, so a reader can place
it against the records above and below it.
reason: threshold when the Spec's trigger was reached, overflow
when the provider refused the prompt for being too long, manual
when something outside the loop asked.
model: which model wrote the summary, which is not necessarily the
agent's own (CompactionPolicy.model). Empty for a compaction
recorded before this was written down.
usage: what the summarising call spent. Already inside
TotalsReport.usage; here so the spend is attributable to the
compaction that caused it rather than only visible in the total.
cost: None when that model had no known price. Never zero
(DESIGN.md §13.2).
| Field | Type | Notes |
|---|---|---|
seq | int | |
reason | Literal | |
replaced_from_seq | int | |
replaced_to_seq | int | |
summary | str | |
model | str | |
usage | Usage | |
cost | `usage.Cost | None` |
at | datetime |
FailureStreakTrip
model
class FailureStreakTrip(*, tool: str, call_id: psych_runtime.core.ids.ToolCallId, streak: int, threshold: int, at: datetime.datetime)The moment one tool's consecutive-failure count crossed the guard's threshold (DESIGN.md §10.6, §23 item 10).
Recorded once per streak, at the call that tipped it over, rather than once per failure afterwards: the guard changes the model's advisory exactly once per streak, and that is the event worth naming. The next success resets the streak; a later run of failures on the same tool trips again.
| Field | Type | Notes |
|---|---|---|
tool | str | |
call_id | ToolCallId | |
streak | int | |
threshold | int | |
at | datetime |
PendingApproval
model
class PendingApproval(*, call_id: psych_runtime.core.ids.ToolCallId, tool: str, arguments: dict[str, typing.Any], question: str | None, expires_at: datetime.datetime)The call a suspended Run is waiting on a decision for.
Everything an approval prompt needs, so a UI does not parse it back out of the suspension's question text: the tool, the exact arguments that will run if approved, and when waiting stops being useful.
| Field | Type | Notes |
|---|---|---|
call_id | ToolCallId | |
tool | str | |
arguments | dict | |
question | `str | None` |
expires_at | datetime |
PendingQuestion
model
class PendingQuestion(*, call_id: psych_runtime.core.ids.ToolCallId, questions: tuple[psych_runtime.core.questions.AskedQuestion, ...], summary: str, expires_at: datetime.datetime)A Run stopped because the agent asked something.
questions carries the structure a console renders: labels to click, and
a description of what each one means. summary is the same thing as one
line, for a notification or a log that will not render options.
The options are suggestions and nothing validates against them. A person may answer in their own words, which is what makes offering them safe.
| Field | Type | Notes |
|---|---|---|
call_id | ToolCallId | |
questions | tuple | |
summary | str | |
expires_at | datetime |
AskedQuestion
model
class AskedQuestion(*, question: Annotated[str, MinLen(min_length=1), MaxLen(max_length=2000)], header: Annotated[str, MaxLen(max_length=24)] = '', options: Annotated[tuple[psych_runtime.core.questions.QuestionOption, ...], MaxLen(max_length=4)] = (), multi_select: bool = False)One question, with the answers the model expects to be useful.
| Field | Type | Notes |
|---|---|---|
question | str | |
header | str | |
options | tuple | |
multi_select | bool |
QuestionOption
model
class QuestionOption(*, label: Annotated[str, MinLen(min_length=1), MaxLen(max_length=80)], description: Annotated[str, MaxLen(max_length=400)] = '')One answer a person can pick without typing.
| Field | Type | Notes |
|---|---|---|
label | str | |
description | str |
Component
value
Type alias.
Type aliases are created through the type statement::
type Alias = int
In this example, Alias and int will be treated equivalently by static type checkers.
At runtime, Alias is an instance of TypeAliasType. The name attribute holds the name of the type alias. The value of the type alias is stored in the value attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.
Type aliases can also be generic::
type ListOrSet[T] = list[T] | set[T]
In this case, the type parameters of the alias are stored in the type_params attribute.
See PEP 695 for more information.
Task
model
class Task(*, title: Annotated[str, MinLen(min_length=1), MaxLen(max_length=200)], active_form: Annotated[str, MaxLen(max_length=200)] = '', description: Annotated[str, MaxLen(max_length=2000)] = '', status: Literal['pending', 'in_progress', 'completed'] = 'pending')One thing the model plans to do, is doing, or has done.
| Field | Type | Notes |
|---|---|---|
title | str | |
active_form | str | |
description | str | |
status | Literal |
TaskStatus
value
def TaskStatus(*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.
Record
value
Type alias.
Type aliases are created through the type statement::
type Alias = int
In this example, Alias and int will be treated equivalently by static type checkers.
At runtime, Alias is an instance of TypeAliasType. The name attribute holds the name of the type alias. The value of the type alias is stored in the value attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.
Type aliases can also be generic::
type ListOrSet[T] = list[T] | set[T]
In this case, the type parameters of the alias are stored in the type_params attribute.
See PEP 695 for more information.
Usage
model
class Usage(*, input: Annotated[int, Ge(ge=0)] = 0, output: Annotated[int, Ge(ge=0)] = 0, cache_read: Annotated[int, Ge(ge=0)] = 0, cache_write: Annotated[int, Ge(ge=0)] = 0, cache_write_1h: Annotated[int, Ge(ge=0)] = 0, reasoning: Annotated[int, Ge(ge=0)] = 0)Token counters for one model call.
Recorded per call and never aggregated at write time. Aggregation is a projection over the log (DESIGN.md §13.4), so a pricing correction can be recomputed while the recorded cost of each call stays as it was.
Attributes:
input: uncached input tokens, billed at the input rate.
output: generated tokens, billed at the output rate.
cache_read: tokens read from the prompt cache, billed at the read rate.
These are not counted in input; the two are disjoint.
cache_write: tokens written to the prompt cache, billed at the write
rate.
cache_write_1h: the subset of cache_write held at one-hour retention
rather than the default five minutes. This is a
subset and not an addition: it is never larger than cache_write,
and the two are not summed.
reasoning: reasoning tokens where the provider reports them separately.
Providers that bill these at the output rate already include them in
output; this field is for visibility, not for a second charge.
| Field | Type | Notes |
|---|---|---|
input | int | |
output | int | |
cache_read | int | |
cache_write | int | |
cache_write_1h | int | |
reasoning | int |
Cost
model
class Cost(*, amount: Annotated[decimal.Decimal, Ge(ge=Decimal('0'))], currency: Annotated[str, MinLen(min_length=3), MaxLen(max_length=3), _PydanticGeneralMetadata(pattern='^[A-Z]{3}$')] = 'USD', model: Annotated[str, MinLen(min_length=1)], source: Literal['computed', 'provider', 'mixed'] = 'computed')What one model call cost, computed at the time of the call.
Written into the Record so it never changes retroactively when the price table is updated (DESIGN.md §13.2). A report six months from now reports what was actually billed, not what the current table would say.
Decimal rather than float: money summed across thousands of calls
accumulates binary rounding error, and a metering system that is off by a
cent per million calls is a metering system nobody trusts.
Attributes:
amount: the cost in currency.
currency: ISO 4217, uppercase.
model: the model id the rates were resolved for, recorded so a later
reader can see which table entry applied.
source: where the number came from. "computed" means Psych derived
it from a PriceResolver; "provider" means the provider or
gateway reported it with the response; "mixed" appears only on
a sum of costs that did not agree.
Recorded because the two are not interchangeable when somebody is reconciling against an invoice. A provider-reported figure comes from the party doing the billing and is authoritative; a computed one is as good as whatever table was in force, which Psych's own docstrings admit goes stale. A total that blended them without saying so would look like one number and be two.
Defaulted to "computed" so every Record written before this
field existed still loads, and still means what it said.
| Field | Type | Notes |
|---|---|---|
amount | Decimal | |
currency | str | |
model | str | |
source | Literal |
Lifecycle
enum
class Lifecycle(*values)Where a Run is, in the words a person would use.
Coarser than TerminalState and finer than the store's RunState:
those answer "how did it end" and "is it claimable", and this answers "what
should the screen say".
| Member | Value |
|---|---|
QUEUED | queued |
RUNNING | running |
WAITING | waiting |
STOPPING | stopping |
DONE | done |
FAILED | failed |
STOPPED | stopped |
TerminalState
enum
class TerminalState(*values)How a Run ended. Every Run reaches exactly one of these.
| Member | Value |
|---|---|
COMPLETED | completed |
FAILED | failed |
ABORTED | aborted |
ABANDONED | abandoned |
FORCE_SETTLED | force_settled |
SuspendReason
enum
class SuspendReason(*values)Why a Run is waiting (DESIGN.md §11).
| Member | Value |
|---|---|
APPROVAL | approval |
QUESTION | question |
EXTERNAL | external |
CHILDREN | children |
ToolOutcome
enum
class ToolOutcome(*values)How a tool call ended.
| Member | Value |
|---|---|
OK | ok |
ERROR | error |
ABORTED | aborted |
UNKNOWN | unknown |
QueueKind
enum
class QueueKind(*values)Where input that arrived mid-Run is parked (DESIGN.md §9).
Three queues rather than one flag, because "stop mid-response and immediately send another request" is three different intentions and collapsing them produces the race that breaks streaming after an interrupt.
| Member | Value |
|---|---|
STEER | steer |
FOLLOW_UP | follow_up |
NEXT_RUN | next_run |
ToolFailure
model
class ToolFailure(*, 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, transient: bool = False, traceback_is_for_the_model: bool = False)A tool failure, as data the model can read.
DESIGN.md §18 states this for code execution and it holds for every tool: a traceback is returned so the model can fix its approach, not raised so it kills the turn.
| Field | Type | Notes |
|---|---|---|
kind | str | |
message | str | |
traceback | `str | None` |
transient | bool | |
traceback_is_for_the_model | bool |
RunId
value
def RunId(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
VersionHash
value
def VersionHash(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
AttemptId
value
def AttemptId(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
StepId
value
def StepId(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
ToolCallId
value
def ToolCallId(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
WorkerId
value
def WorkerId(x, /)NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage::
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str: ...
UserId('user') # Fails type check
name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int