pydantic-ai-algenta
pydantic-ai tool integration for Algenta: AlgentaToolset, a
WrapperToolset that wraps a
pydantic_ai.mcp.MCPToolset pointed at your own
self-hosted Algenta Engine, and layers on:
- Tool-profile filtering -- expose only
observe(read-only, the default),govern,execute, or the opt-infullregistry, percontracts/integration-tool-contract.json. - A typed
execute_decisionresult --execute_decision's result is parsed into anExecutionReceipton success, so your code gets a typed object instead of an untyped dict. Every other tool's result (plan_decision,log_decision,get_contract, ...) is freeform and passes through unchanged -- there is no shared envelope every tool returns. - Native denial handling --
execute_decisionis fully synchronous: a call either succeeds or is blocked in the very same call by one of three named policy gates. A blocked call surfaces through pydantic-ai's own denial primitive (ToolDenied, the same thing a human reviewer's "no" produces) with the real gate name preserved -- not a bespoke mechanism, and not pydantic-ai's deferred-tool-approval primitives, since there is nothing asynchronous to pause on. See The execute_decision result below.
Install
pip install pydantic-ai-algenta
This package depends on exactly two things: the published
algenta-sdk and
pydantic-ai-slim[mcp] (which pulls in
fastmcp's client, since that's what pydantic-ai's own MCP support is
built on). It never depends on, imports, or bundles any part of the Algenta Engine itself.
Self-hosted-first
AlgentaToolset talks to your own self-hosted Algenta Engine over its MCP endpoint --
never a hosted-by-Algenta cloud service. The endpoint resolves, in order, from:
base_url=passed to the constructor,- the
ALGENTA_BASE_URLenvironment variable, http://localhost:8000/mcp(the default for a local self-hosted engine).
Quick start
Prerequisites:
- A running self-hosted Algenta Engine, reachable over MCP -- defaults to
http://localhost:8000/mcp; point elsewhere viaALGENTA_BASE_URLor the constructor'sbase_url=. No Algenta account or Algenta-issued API key is ever needed: Algenta isn't a hosted service you sign up for. - An API key for whichever model you pass to
Agent(...)-- the example below uses"openai:gpt-5", which needsOPENAI_API_KEYset in your environment.
Don't have a self-hosted engine running yet? Try it locally below runs the same toolset end to end with neither an engine nor a model API key.
import asyncio
from pydantic_ai import Agent
from pydantic_ai_algenta import AlgentaToolset
async def main() -> None:
# Talks to your own self-hosted engine (ALGENTA_BASE_URL, or the constructor arg below).
toolset = AlgentaToolset(base_url="http://localhost:8000/mcp", profile="observe")
agent = Agent("openai:gpt-5", toolsets=[toolset])
result = await agent.run("What's the expected value of scenario X?")
print(result.output)
asyncio.run(main())
Already inside an async context (a notebook cell, an async web handler)? Drop
asyncio.run(main()) and await main() (or inline main's body) instead --
asyncio.run is only valid
at the top level of a plain script.
profile="observe" is also the default if you omit it -- the agent can call
get_contract / query_data / simulate / recommend, and nothing that plans, logs, or
executes anything. See Tool profiles to opt into more.
Try it locally (no live engine required)
This repository's test suite includes a real stub Algenta MCP server
(tests/stub_server.py) -- a genuine
fastmcp.FastMCP server over a real local HTTP socket, not a mock --
plus TestModel, a real pydantic-ai model that
scripts an agent's tool-calling deterministically without calling any LLM provider. Together
they let you run a full AlgentaToolset agent turn with no self-hosted engine and no model
API key, straight from a checkout of this repository:
git clone https://github.com/thyn-ai/algenta-integrations
cd algenta-integrations/python
uv sync --package pydantic-ai-algenta --all-extras
uv run --package pydantic-ai-algenta python pydantic-ai-algenta/local_demo.py
local_demo.py is a short, real script (not a snippet to paste) that starts
the stub server, points an AlgentaToolset at it, and runs one scripted agent turn:
import asyncio
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
from pydantic_ai_algenta import AlgentaToolset
from tests.stub_server import StubServerFixture # this repo's own test stub; not part of the published package
async def main() -> None:
async with StubServerFixture() as server:
toolset = AlgentaToolset(base_url=server.base_url, profile="observe")
# TestModel scripts which tool gets called -- no OPENAI_API_KEY, no network call to
# any LLM provider. Swap in a real model (e.g. Agent("openai:gpt-5", ...)) once you
# have both a live engine and a model API key -- see Quick start above.
agent = Agent(TestModel(call_tools=["recommend"]), toolsets=[toolset])
result = await agent.run("What should we do about scenario X?")
print(result.output)
asyncio.run(main())
Running it prints a real tool result from the stub server, e.g.
{"recommend":{"scenario":"a","recommended_action":"hold","confidence":0.87}}. (You may also
see a harmless asyncio.exceptions.CancelledError traceback logged during the stub server's
shutdown, after the printed result -- that's the demo tearing down its local HTTP server, not a
failure.)
local_demo.py isn't part of the published pydantic-ai-algenta PyPI package -- it imports
tests.stub_server, which only exists in a checkout of this repository.
Tool profiles
| Profile | Adds | Notes |
|---|---|---|
observe (default) |
get_contract, query_data, simulate, recommend |
Read-only. |
govern |
+ plan_decision, log_decision |
Propose/record decisions; never executes. |
execute |
+ execute_decision |
Real-world execution -- see below. |
full |
everything the connected engine advertises | Opt-in only; admin/ops tooling. |
toolset = AlgentaToolset(base_url="...", profile="execute")
An observe-profile toolset's get_tools() genuinely does not list execute_decision (or
anything govern/execute-tier) -- it's not just undocumented, the model has no way to know it
exists. force / override_safety (operator/break-glass-only fields on execute_decision's
real schema) are never exposed either, in any profile: stripped from the advertised JSON schema
and scrubbed from the arguments dict actually forwarded to the wrapped MCP call, in case
something upstream still tried to pass one.
The decision lifecycle
The real lifecycle behind the govern/execute profiles is: plan_decision(...) produces a
freeform, not-yet-committed plan summary; log_decision(chosen_action, ...) persists a decision
record and returns its decision_id; execute_decision(decision_id, webhook_url, ...) dispatches
that already-logged decision for real-world execution (a webhook delivery) and returns an
execution receipt. There is no separate, model-reachable approval step in between -- a genuinely
separate human-approval system exists on the engine side (plan/case/analysis-run review), but the
engine's own MCP tool registry does not expose it as a tool at all, by design, so no integration
package -- this one included -- can wire up a flow around it.
The execute_decision result
execute_decision is real-world execution, and it is fully synchronous: every call returns
either a success or a named denial in that same call -- never "pending, check back later".
AlgentaToolset.call_tool maps whichever one comes back onto pydantic-ai's own primitives:
-
Success -- the result validates as an
ExecutionReceiptand is returned as-is:from pydantic_ai_algenta import ExecutionReceipt receipt: ExecutionReceipt = tool_return_part.content receipt.decision_id receipt.webhook_url receipt.execution_status # "delivered" | "failed" receipt.response_code receipt.safety_overridden # True if a human operator's force/override_safety applied
Note that
execution_status == "failed"-- the downstream webhook delivery itself failed -- is still a successfulexecute_decisioncall. The engine did what was asked and is honestly reporting the outcome; it isn't refusing the call, so this is not a denial. -
Denial -- the engine synchronously blocks the call with one of exactly three named policy gates, and
AlgentaToolset.call_toolreturnsToolDenied(which pydantic-ai turns intoToolReturnPart(outcome="denied")), with the real gate name and the engine's own message/override hint preserved in the denial message:Gate Meaning Bypass idempotencyThis decision_idhas already been delivered.force=true, for one re-execution. Operator-only; never model-facing.confidenceThe decision's confidence is below policy.min_confidence.override_safety=true. Operator-only; never model-facing.risk_floorrisk_p5is below-policy.risk_floor.override_safety=true. Operator-only; never model-facing.A human operator applying one of those bypasses does so outside the model-facing tool call entirely (their own direct call to the engine, or a break-glass path in your own code) -- never by the model setting
force/override_safetyitself, which is exactly what the never-model-facing scrubbing above prevents. -
Anything else -- a transport/HTTP-level failure (a dropped connection, a 5xx, a timeout) is not this package's concern to map: it already surfaces as whatever pydantic-ai's own
MCPToolsetraises (typicallyToolFailedorModelRetry) beforeAlgentaToolset.call_toolever gets a result to parse.
Why not ApprovalRequired / DeferredToolRequests?
An earlier version of this package modeled execute_decision as pausing for an out-of-band
approval, surfacing that pause through pydantic-ai's deferred-tool-approval primitives
(ApprovalRequired -> DeferredToolRequests -> DeferredToolResults). That was wrong: checked
directly against the real engine, execute_decision has no asynchronous "pending" state at
all -- a call either succeeds or is blocked by a named gate in the very same call, and the
engine's separate, genuine human-approval system for decision plans is explicitly not exposed as
an MCP tool. There is nothing for this package to pause on, so this version removes that
machinery entirely rather than keep it dormant for a case that cannot occur. A blocked
execute_decision call is a deliberate "no" decided synchronously by the engine, and
ToolDenied -- pydantic-ai's own primitive for exactly that -- is the correct, and simpler,
fit.
Typed receipts
Every tool other than execute_decision returns its own freeform result and passes through
AlgentaToolset unchanged -- there is no shared "governed execution" envelope every tool
returns. execute_decision's successful result is the one exception, always parsed into an
ExecutionReceipt; see The execute_decision result
above.
Testing this package's own test suite (not your agent)
The test suite (tests/) runs a real
fastmcp.FastMCP server over a real local HTTP socket -- a
deliberately fake, deterministic stand-in for a self-hosted Algenta MCP endpoint, never a real
engine (none is reachable in CI) -- and drives it with a real AlgentaToolset /
pydantic_ai.mcp.MCPToolset / pydantic_ai.Agent, using
TestModel to script tool-calling deterministically.
ALLOW_MODEL_REQUESTS = False is set in tests/conftest.py as a real, enforced guard against a
test accidentally calling a live model.
cd python
uv sync --all-packages --all-extras
uv run --package pydantic-ai-algenta pytest pydantic-ai-algenta/tests -v
(fastmcp's full/server-side package and anyio's pytest plugin are dev-only extras of this
package -- neither is a runtime dependency of AlgentaToolset itself.)
Metadata
Release files for pydantic-ai-algenta 0.1.3
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_algenta-0.1.3.tar.gz | 29.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_ai_algenta-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.5 kB
Release files / pydantic_ai_algenta-0.1.3.tar.gz
| Download URL | pydantic_ai_algenta-0.1.3.tar.gz |
|---|---|
| Size | 29.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
444757b59727bec6a4d68aa600dbe72337db753e1ac2d8714e1294c81888ec10
|
|
BLAKE2b-256 checksum How to use checksums |
8a7b4b5110654a810c9f413f430c2fee1e9374c23a8f73f666a246164ff6886a
|
| 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_algenta-0.1.3-py3-none-any.whl
| Download URL | pydantic_ai_algenta-0.1.3-py3-none-any.whl |
|---|---|
| Size | 21.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8b73eaa03d29b182935d1fe7f0b91fa3ea9d3306734126bb3d89bd240250e49c
|
|
BLAKE2b-256 checksum How to use checksums |
24ec543a58d96092dc69326033bf14ccbd4d6395b1347347dd6b9a83f8f76530
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|