Skip to main content

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.

PyPI Python Pydantic AI License integration tests Governor's Log

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_at gates the undeclared-scope violation (POL-001) on scope assertions; never suppresses it.
  • task_boundary signals are evaluated over the declared target_system values of successive tool calls.
  • high_consequence.tools patterns are matched against <tool name>:<target_system>, for example crm_fetch:crm, and attach the advisory HIGH_CONSEQUENCE_DETECTED flag.
  • high_consequence.operations rules do not apply to Pydantic AI tool calls. Those rules match the semantic classification the core package derives for Claude Code Bash commands (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_id means 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)

Source distribution for pydantic-ai-governor 0.1.1
File Size Uploaded
pydantic_ai_governor-0.1.1.tar.gz 76.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pydantic-ai-governor 0.1.1
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page