Sentience Governor for Pydantic AI
Agent Execution Evidence at the Pydantic AI runtime execution boundary.
Governance-relevant evidence of what an agent actually dispatched at runtime, against the declaration it recorded before the run. This is a different artifact from logging, tracing or observability, and it is meant to sit alongside them rather than replace them.
Sentience Governor itself needs no Sentience account and no Sentience API key. Whatever model or provider you give Pydantic AI has its own credential requirements, and this changes none of them. By default, your traces stay on your machine.
What's new
0.1.1 resolves, for each run, the governance profile the operator
intended for that agent. With a ~/.sentience/resolution.yaml the first
agent_id pattern that matches is authoritative; with none, the machine
default applies; with neither, the run proceeds with no profile, exactly as
0.1.0 did. Resolution happens once at session open and is recorded on the
registration (bound or degraded, with the matched pattern), and every
event of a governed run carries the profile's fingerprint. A bound profile
shapes what is evaluated: demand_at gating of the undeclared-scope
violation, task-boundary signals over the declared targets, and
high_consequence.tools patterns over <tool name>:<target_system>.
Operations rules do not apply to Pydantic AI tool calls, which carry no Bash
operation classification. A broken profile never interrupts a run: a matched
binding whose file is missing, unparseable or not valid degrades to the
default, a broken default leaves the run on no profile, and either case is
one warning at session open. Requires core Sentience Governor 0.3.2.1 or
later. See Which profile governs a run.
This README lists releases of pydantic-ai-governor only.
sentience-governor is versioned and released separately, and its releases
do not appear here.
What Agent Execution Evidence means
The question a governance reviewer asks is did this agent do what it said it was going to do? Answering it needs two things recorded together. The declaration: what this run said it was for, or that it declared nothing, recorded before the run starts. The dispatch record: each tool call the framework validated and was about to execute, recorded at the boundary where execution actually happens.
A tracing system can carry either of those, and a modern one can carry arbitrary metadata, so this is not a claim about what tracing is capable of. The difference is what this integration records by default and where. It records the declaration first, before anyone knows what the agent will do, then records each dispatch at the execution boundary, and keeps the two joinable. A tool call outside the declared scope is visible as such because of that pairing, not because a field was named well.
Sentience Governor records. It does not intervene. See What the evidence does and does not prove.
Installation
pip install pydantic-ai-governor
This installs sentience-governor and pydantic-ai-slim as dependencies.
See Compatibility for the supported version ranges.
It does not install model or provider SDKs, and it is not meant to. Your
Pydantic AI application already declares whichever provider it uses, through
Pydantic AI's own extras or a full pydantic-ai install. Adding this package
to an application that already runs changes nothing about that.
In a fresh environment, install the provider your example needs alongside it. For the OpenAI model used below:
pip install pydantic-ai-governor "pydantic-ai-slim[openai]>=2.37.0,<2.38"
Attaching the capability
from pydantic_ai import Agent
from pydantic_ai_governor import SentienceGovernor
agent = Agent(
"openai:gpt-4o",
capabilities=[SentienceGovernor()],
)
result = await agent.run("Reconcile the August invoices")
That is the whole integration. Every run of this agent now opens its own Sentience Governor session and writes a trace.
Constructor defaults
The constructor takes three optional keyword arguments, and every one has a usable default:
SentienceGovernor(
objective=None, # what runs of this agent are for
scope=None, # the systems runs of this agent may touch
agent_id="pydantic-ai-agent",
)
objective and scope here are defaults for every run, suitable when an
agent has one standing purpose. agent_id names the agent in the evidence;
change it when one process runs several distinct agents and you want them
distinguishable.
Every session records its declaration state before execution, including the absence of a valid declaration. A run with no objective from either source is recorded as having declared none. That is a supported state and a real reading, not a gap in the record, and nothing invents an objective to fill it.
Per-run objective and scope
Most agents do different things on different runs. Declare per run, and the run's declaration overrides the constructor defaults:
result = await agent.run(
"Reconcile the August invoices",
metadata={"sentience_governor": {
"objective": "Reconcile August invoices",
"scope": ["crm", "billing"],
}},
)
Both keys are optional and fall back key by key: a block that supplies only
objective keeps the constructor's scope.
A malformed block is never silently ignored. If the block is not a
mapping, if a key is misspelled, or if a value has the wrong type, the run
continues normally and you get a UserWarning at the keyboard naming the
field and the contract it broke. The warning never reproduces the value you
wrote, since a rejected declaration is exactly where sensitive text might
have been.
What governs the run after a rejection depends on your constructor. The
malformed block is rejected atomically, so a good objective beside a broken
scope yields no half-declaration. Then:
| Constructor supplied | After a malformed per-run block |
|---|---|
A valid objective (and scope) |
Those defaults remain in force, and the run is declared under them |
| Nothing | The run is recorded as undeclared |
So a rejected block does not by itself make a run undeclared. It falls back, and the warning says which of the two happened.
Classifying tools
A tool call is recorded whether or not you classify it. Classification is how you tell the evidence what the call is:
from pydantic_ai.tools import Tool
crm_tool = Tool(
crm_fetch,
metadata={"sentience_governor": {
"operation": "READ", # READ, WRITE, DELETE or EXECUTE
"target_system": "crm", # matched against declared scope
"classification": ["internal"], # data classifications you assert
}},
)
All three keys are optional. operation must be one of the four values
exactly, in upper case: "read" is not "READ", and accepting the near miss
would mean recording something you did not write.
When classification is missing
This is the case worth understanding, because the honest answer is not the convenient one.
Nothing is inferred from the tool's name. A tool called
db_delete_record does not become a delete against a database. Name-based
inference would put a guess into a record whose whole value is that it
contains no guesses.
So an unclassified call is recorded as unclassified. The target_system
falls back to the tool's own name, which is a fact about the call rather than
a bucket someone guessed, and the snapshot is flagged as unclassified, which
is what surfaces in review.
What the operation field says, if you read the raw trace
When no operation is declared, the integration treats it internally as
UNKNOWN. Current Sentience Governor core cannot serialize UNKNOWN:
operation_type is required and has four members. At the core boundary the
undeclared case is therefore written as:
operation_type |
asserted_permissions |
|
|---|---|---|
Explicitly declared READ |
READ |
["read"] |
| No operation declared | READ |
[] |
That READ is a compatibility representation, not an observed read. It
does not mean the tool read anything. READ is used because it is the only
non-mutating member, so an undeclared call is not written as a mutation the
developer never claimed.
The empty asserted_permissions is what distinguishes the two rows for a
human reading the trace. It is not a mechanism: no policy rule or analyzer
in this release interprets empty permissions as UNKNOWN, and nothing here
should be read as saying they do. The mapping exists only while core has no
undeclared-operation semantic. Once Governor core provides a first-class
one, a future release of this integration can remove the mapping; a
published release does not change how it serializes on its own.
A malformed classification is not the same as a missing one, and the two
are kept apart. A developer who wrote metadata believes they classified
something, so an invalid value in a recognized field rejects the block
atomically: a good operation beside a broken classification yields
nothing, you get a UserWarning and a governance error naming the field,
and the call falls back to unclassified.
One narrower case: an unrecognized key alongside otherwise valid fields is reported, and every field that independently validates is kept. Discarding a truthful classification because of an unrelated stray key would make the evidence worse while protecting nothing.
Sessions
One run, one session. A Pydantic AI run's own run_id is the Sentience
Governor session id, so the two systems agree on identity with no mapping
table to drift. When the run ends, on the success path or the error path
alike, the session ends.
Concurrent runs stay separate. Sibling runs of one agent, in flight together on one event loop, each get their own session and their own trace file. Neither can pick up the other's tokens, model, provider or turn identity, and per-session sequence numbers stay unique and gapless within each. The same holds for parallel tool calls inside a single model response: each call carries its own identity, and no per-call state is shared between them.
A resumed run is a new session, deliberately. When a deferred tool call
is approved and you resume, Pydantic AI starts a new run with a new run_id,
so Sentience Governor opens a new session. The two are not merged. This
release introduces no cross-run correlation, and presenting two runs as one
session would be a claim about continuity that nothing here verifies.
Which profile governs a run
Since 0.1.1 each run resolves the governance profile that applies to it,
using the core package's own resolution (Sentience Governor 0.3.2.1 or
later), keyed on the capability's agent_id. Resolution happens once, when
the session opens, and the result is fixed for the life of the run. There is
no profile= argument: which profile applies is the operator's decision,
made in ~/.sentience/, not the developer's at construction time.
The chain, in order, with no other steps:
| Outcome | When | What the run gets | Recorded on the registration |
|---|---|---|---|
bound |
~/.sentience/resolution.yaml exists and the first agent_id pattern that matches names a profile that loads and is valid |
that profile | profile_resolution: bound, profile_binding: <pattern> |
degraded |
the first matching pattern names a file that is missing, unparseable or not valid | the machine default ~/.sentience/profile.yaml if it exists and is valid, otherwise no profile; a later binding is never consulted |
profile_resolution: degraded, profile_binding: <pattern> |
default |
no resolution file, or no pattern matches | ~/.sentience/profile.yaml |
nothing extra: the registration is the same as before 0.1.1 |
none |
no match and no default, or a default that is unparseable or not valid | no profile, the 0.1.0 behaviour | nothing extra |
A profile that parses but is not valid (a wrong type where the runtime reads
a value, a mapping key that is not a string) is treated exactly like one that
could not be loaded: it is never bound. The core validator decides; this
package does not re-implement the schema. As a second check, a profile the
resolver hands back is used only if it exposes its sections and fingerprint
and passes core's validate(); otherwise the run continues with no profile
and the registration says so.
Every resolution problem is one warning, once, at session open. A
degraded binding, an invalid or unparseable default, a malformed resolution
file (ignored, the default step still applies) or a malformed binding entry
(skipped) produces one UserWarning for the developer and one
GOVERNANCE_ERROR record with agent_continued: true, and then nothing
more for the rest of the run. Nothing raises into the Pydantic AI run.
What a bound profile changes. Once a profile is bound, the core package's existing profile-driven evaluation applies to this integration's events exactly as it does for the core MCP wrapper:
session_intent.demand_atgates the undeclared-scope violation (POL-001) on scope assertions;neversuppresses it.task_boundarysignals are evaluated over the declaredtarget_systemvalues of successive tool calls.high_consequence.toolspatterns are matched against<tool name>:<target_system>, for examplecrm_fetch:crm, and attach the advisoryHIGH_CONSEQUENCE_DETECTEDflag.high_consequence.operationsrules do not apply to Pydantic AI tool calls. Those rules match the semantic classification the core package derives for Claude CodeBashcommands (operation_classification); this integration records developer-declared execution evidence and emits no such classification, so an operations rule has nothing to match and never fires here. A profile that carries only operations rules still binds and its fingerprint is recorded; the rules are inert for these events.
Every event of a run under an operator-authored profile carries that
profile's 12-character profile_fingerprint, so traces correlate to the
policy that produced them. The full content hash never appears in this
integration's evidence, and no snapshot file or sidecar is written: one run
is one process, so the resolved profile is sticky by construction. Editing
resolution.yaml or a profile file changes nothing for a run already open;
the next run resolves fresh.
As everywhere in Sentience Governor, a profile shapes what is evaluated and recorded. It does not halt, refuse, delay or alter a tool call.
What evidence is produced
Each session writes one append-only JSONL file:
~/.sentience/traces/pydantic-ai/<run_id>.jsonl
Within it, per session:
| Record | When |
|---|---|
| Agent registration | Session open |
| Declared intent | Session open, recording the declaration state: the objective and scope, or that none was declared |
| Scope assertion | After validation, immediately before a tool is dispatched, keyed by tool_use_id |
| Context snapshot, per tool | After a tool returns normally, keyed by the same tool_use_id |
| Context snapshot, per model turn | After each model response, carrying that turn's measured token usage, model and provider identity, and the tool call ids that turn issued |
The ordering carries meaning. A scope assertion is written after the framework has validated the call and immediately before dispatch, so it records a call that really was about to run. The tool snapshot is written only on a normal return.
Read the pair by identity, not by position. A model response can issue
several tool calls at once, so "the next snapshot" is not necessarily the one
belonging to a given assertion. The join is tool_use_id:
A scope assertion with no matching tool context snapshot for the same
tool_use_idmeans no normal return was observed for that call.
That is the whole claim. It does not say why the call failed to return normally, and it does not distinguish a raised exception from anything else. There is still no execution-outcome field in the schema, and none is being inferred here.
Estimated context, measured usage
Two numbers on these snapshots share a field name and are not the same kind of evidence. Keeping them apart matters if you sum them.
| Snapshot | context_size_tokens |
Source |
|---|---|---|
| Per tool | Estimated | The established Sentience Governor estimator, the same one the shipped MCP wrapper uses. A tool boundary has no measured model-input count |
| Per model turn | Measured | ModelResponse.usage.input_tokens |
Per-turn llm_prompt_tokens and llm_completion_tokens are also measured,
from ModelResponse.usage. On a model-turn snapshot, context_size_tokens
and llm_prompt_tokens carry the same measured number: one measurement under
two field names, not two independent readings.
An estimate and a measurement are not interchangeable. Do not add a tool snapshot's estimated context to a model turn's measured usage and present the total as measured token spend.
Governance errors, including the malformed-metadata cases above, are routed by the core package to stdout rather than into the trace file. That is the core package's routing decision, and this integration follows it rather than working around it.
The trace uses the normal Sentience Governor format, and compatibility was
verified against the analyzers exercised in this release's test suite:
compute_pulse, the undeclared-intent analysis, and the token and tool
attribution paths those cover. Analyzers outside that set are untested here
rather than known to differ.
What the evidence does and does not prove
It records that a tool call passed the framework's validation and was dispatched, within a session whose declaration state was recorded before the run began, with the measured token usage of each model turn.
It does not prove the call succeeded. A scope assertion with no matching
tool snapshot for the same tool_use_id means no normal return was observed
for that call. It does not say why.
It does not establish object-level scope. target_system is a declared
label, not a verified assertion about which records or rows a call touched.
A call declared against crm is recorded as such; nothing here checks that
it stayed inside any particular customer's data.
It does not intervene. This capability records and flags, and takes no action on a tool call before or after it runs: nothing is halted, refused, delayed or altered. It must not be relied on as a control. Flags are advisory signals for review.
It does not rank or grade what it recorded. A flag says what was observed. Deciding what that is worth is the reviewer's job, and a number attached here would only be a guess wearing a decimal point.
It says nothing about a run it did not observe. Evidence is bounded to sessions where the capability was attached.
Observation and non-interference
Across the tested execution paths for this release, attaching the capability did not change agent output, message count, token usage, exception propagation, retries, deferral, streaming or control flow. Those paths are: normal returns, text-only runs, parallel tool calls in one model response, retries, raised tools, validation failures, streaming, deferred and resumed execution, and concurrent runs. Each is compared against the same run without the capability attached, and a raised tool propagates the same exception type with the same message. The model response is returned exactly as it arrived.
That is evidence from a tested surface, and it is stated that way on purpose. It does not assert the same about every path a Pydantic AI agent can take, only about the ones listed above.
Non-interference is about the agent, not about silence. Malformed metadata deliberately produces a warning and a governance error. That is additive evidence plus a message to the developer, and it changes nothing about tool returns, agent output, retries, deferral, streaming or control flow of any kind.
Compatibility
| Requires | Range |
|---|---|
sentience-governor |
>=0.3.2.1,<0.3.3 |
pydantic-ai-slim |
>=2.37.0,<2.38 |
| Python | >=3.10 |
These bounds are deliberate published compatibility contracts rather than
defaults, and both ceilings are narrow on purpose. The sentience-governor
floor is 0.3.2.1, the first core release whose profile validation covers
every field the runtime reads and whose resolver never raises into a run;
per-agent resolution in this release is validated against it. A wider
sentience-governor ceiling would let a future core release change this
distribution's observable behavior without a new release of it. A wider pydantic-ai-slim ceiling would
assert compatibility with releases this integration has not yet verified,
including ones published after this release.
They are widened only after measured verification against a new version, and only in a new release of this distribution. A bound is never relaxed in place.
Continuous integration runs the suite on Python 3.10, 3.11, 3.12 and 3.13, with separate floor and ceiling dependency legs.
Today both legs resolve to pydantic-ai-slim==2.37.0, because no later
2.37.x release has been published inside the <2.38 ceiling. The legs are
kept separate on purpose: when a compatible 2.37.x release appears, the
ceiling leg picks it up automatically and the range is exercised at both ends
without anyone editing the workflow.
Relationship to sentience-governor
This is an independent distribution that depends on the core package. The dependency runs one way only:
pydantic-ai-governor -> sentience-governor
-> pydantic-ai-slim
Core acquires no Pydantic AI dependency, mandatory or optional. The two distributions have independent versions, changelogs, artifacts, tags and release trains, and both follow the same release discipline. Releasing one does not require rebuilding, versioning, tagging or republishing the other.
pydantic-ai-governor is a distribution name following the ecosystem's
pydantic-ai-<name> convention. It does not imply that Pydantic owns,
operates or endorses Sentience Governor.
Where the pieces live: the core package is
sentience-governor on PyPI,
with its documentation, changelog and the other integrations (Claude Code,
MCP clients, LangChain) in the
repository; this
distribution's source is in that repository under
integrations/pydantic-ai-governor. The product, its positioning and
Governor's Log are at getsentience.ai, which
remains the destination for everything that is not a package page.
Governor's Log
The reasoning behind how Sentience Governor records, and what we are learning from pointing it at real agent sessions, is published as Governor's Log: a technical publication about AI agents, their architectures, and the systems needed to understand and govern them.
License
Apache-2.0. See LICENSE.
Metadata
Release files for pydantic-ai-governor 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pydantic_ai_governor-0.1.1.tar.gz | 76.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_ai_governor-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 108.9 kB
Release files / pydantic_ai_governor-0.1.1.tar.gz
| Download URL | pydantic_ai_governor-0.1.1.tar.gz |
|---|---|
| Size | 76.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e2c266ef46de0f438e9c1d15a5f7c8b6739f636b69177bef184ba19314138b4
|
|
BLAKE2b-256 checksum How to use checksums |
f91345c673f69a99a8273eed0d093e7a390b4b7adcf8e9374a9563af8daed5fe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / pydantic_ai_governor-0.1.1-py3-none-any.whl
| Download URL | pydantic_ai_governor-0.1.1-py3-none-any.whl |
|---|---|
| Size | 32.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
74788083c31390a1baf96d8cb1652d88cfc694f0527cde8390c84606981bd956
|
|
BLAKE2b-256 checksum How to use checksums |
1c49a5a96d11140f877fe6070efff488a18eac1d64495624121540b1db7cbbea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|