Frozen contracts
These are the seams where slices otherwise drift: the invariants the codebase holds frozen. They are verified against pydantic-ai 1.107 and enforced across the backend services.
!!! danger “Do not violate” Each of these is a contract other parts of the system rely on. Changing one means re-checking every place that depends on it.
Usage & cost
- Usage is recorded per
ModelResponse, never per run.RunUsage/RequestUsagehave nomodel_name(it’s onModelResponse.model_name). Iterateresult.all_messages(), pairing eachModelResponse.usagewith its.model_name. One row perModelResponse(INSERT ... ON CONFLICT (run_id, request_index) DO NOTHING). - Usage idempotency =
UNIQUE(run_id, request_index), whererequest_indexis the deterministic NthModelResponse: notprovider_response_id(nullable), not a Temporal event count. The counter survives Continue-As-New via the CAN input. - Cost:
genai-pricesis primary, with a versionedmodel_pricingtable for audit (pricing_idpinned).
Streaming & transport
- AG-UI is the only streaming envelope, on the Redis bus and the SSE wire.
Both the inline and the durable paths run the same executor and emit events
through the single pydantic-ai-event → AG-UI converter
(
realtime/protocol/converter.py), so the two paths produce byte-identical event sequences (a conformance test pins this).run_stream*/iterare forbidden inside a Temporal workflow. - Redis STREAMS for the token channel (replay via entry-id = Last-Event-Id);
Pub/Sub only for control/presence. One key convention:
personal_agent.contracts.keys.
Durable runs
- BYOK + Temporal: the durable path passes the model as a string in the
RunSpec; the worker registers decrypted model instances once at startup (from the admin platform keys) and resolves them by theirprovider:modelid inside the activity (resolve_model). Keys never enter workflow history. - Toolsets are snapshotted into the
RunSpecat run start; the workflow never queries live DB state during a run/replay. MCPid=and agentname=are stable. - Agent
name = template.slugfor inline + durable; model/toolsets/instructions are per-run.
Data & schema
- Extensions (vector/pgcrypto/citext) are created by CNPG
postInitSQLas superuser, never in Alembic (the app role is unprivileged).
Resilience & readiness
- Readiness gates only on hard deps (DB, Redis).
/readyzchecks only those hard deps;/healthzis pure liveness (never touches a dependency); Temporal and JWKS are reported as soft deps on/health/depsbut never gate readiness, so a Keycloak/Temporal blip can’t take the API offline.
Auth, tenancy & transport security
- SSE/WS auth: the WS token travels via the
Sec-WebSocket-Protocolsubprotocol (the server echoes the selectedbearersubprotocol on accept), never the query string. SSE has a server-enforced max lifetime (security.sse_max_lifetime_seconds, ≈ token expiry); on expiry the stream emits a reconnect event and closes. - Tenancy: validate
X-Personal-Agent-Orgagainst the token org claim every request; Postgres RLS as defense-in-depth (ScopedDbDepsetspersonal_agent.current_org).
Governance & safety
- Untrusted content gates tools: when any untrusted toolset (a
user-registered MCP / OpenAPI integration, whose output is a prompt-injection
vector) is in a run, the assembler drops high-privilege first-party/device
tools via
toolset.filtered()(apply_untrusted_gateinassembler/policy.py). The durable path runs the same assembler, so the gate is identical per-request. - Data-classification is fail-closed: tagged data must never reach a provider
not cleared for it. The same
auto_model.enforce_classificationgate runs at every model-resolution entry (inline, durable chat, triggered workflows, comms triage). - No secrets in spans: content capture defaults off; provider keys never
appear in
ModelSettingsdumps, errors or Temporal inputs.