concordat
Typed sessions for AI agents. Agents are objects whose interfaces are protocols, and the runtime enforces those protocols on the model.
Status: pre-alpha. Agent classes, the protocol/session/budget kernel, and runners for Claude (Anthropic) and OpenAI work. See docs/design.md.
Why
"Never refund before verifying identity" usually lives in a prompt and is hoped for. Concordat makes it a checked protocol:
- Protocol fidelity: the session follows the declared order of turns.
- Least privilege per step: the model only gets the tools legal in the current state.
- Bounded spend: tokens and tool calls are charged automatically and stay within
budget, even across handoffs. A dollar budget is charged by your code (
budget.spend(usd=...)), since the runners don't know model prices; a run stops when any limited amount reaches zero.
Evidence
On τ-bench's 50 airline tasks, with simulated customers who push the agent to skip confirmation ("just do it and stop asking"), counting changes made without the explicit "yes" the airline policy requires:
| Agent model | Tasks passed | Changes made without a "yes" | |
|---|---|---|---|
| Claude Haiku 5.5 (3 runs) | standard agent | 112 / 128 | 50 (in 29 task-runs) |
| concordat agent | 105 / 128 | 0 | |
| gpt-4.1 (1 run) | standard agent | 20 / 50 | 26 (in 13 tasks) |
| concordat agent | 22 / 50 | 0 |
The standard agent broke the rule with both models; concordat never did. With Haiku, concordat sometimes transferred a customer who kept refusing to say yes, costing some task success; after telling it to ask again instead, it matched or beat the standard agent with gpt-4.1 (not yet rerun with Haiku). With cooperative customers neither agent broke the rule. Setup, limits and all runs: bench/RESULTS.md.
Example
from concordat import Agent, Budget, human, tool
class RefundAgent(Agent):
final = {"refunded", "denied"}
budget = Budget(tool_calls=10)
@tool(when="start", then={True: "verified", False: "denied"})
def verify_identity(self, user_id: str) -> bool:
return lookup(user_id).verified
@tool(when="verified", then="awaiting_approval")
def propose_refund(self, amount: float) -> float:
return amount
@human(when="awaiting_approval", approve="refunded", reject="denied")
def approve_refund(self, amount: float) -> str:
return f"Refund ${amount}?" # shown to the human approver
agent = RefundAgent() # protocol already checked when the class was defined
agent.allowed_tools() # {'verify_identity': ...}: all the model gets to see
agent.propose_refund(5000) # ProtocolViolation, raised before the body runs
See the process
RefundAgent.diagram() draws the protocol, straight from the class:
stateDiagram-v2
direction LR
state "awaiting_approval" as s0
state "denied" as s1
state "refunded" as s2
state "start" as s3
state "verified" as s4
[*] --> s3
s0 --> s1: approve_refund (human rejected)
s0 --> s2: approve_refund (human approved)
s3 --> s1: verify_identity
s3 --> s4: verify_identity
s4 --> s0: propose_refund
s1 --> [*]
s2 --> [*]
Safe inheritance
Specialise an agent by subclassing it. Adding steps is fine; bypassing the parent's is rejected when the class is defined:
class FastRefundAgent(RefundAgent):
@tool(when="start", then="refunded")
def instant_refund(self, order_id: str) -> str: ...
# ProtocolError: FastRefundAgent breaks RefundAgent's protocol: after instant_refund,
# it reaches 'refunded', which RefundAgent only reaches through
# verify_identity → propose_refund → approve_refund
So a team can define a compliant base agent once, and every specialised version keeps its safety steps.
Running it with Claude
pip install "concordat[anthropic]"
import anthropic
from concordat.anthropic import run
result = run(RefundAgent(), anthropic.Anthropic(), "Please refund order A-100.")
result.status # "done", or "waiting" if Claude asked the user something
Each turn Claude is offered only the actions legal in the current state (a human
step appears as a request that only the approver can decide), one call per
response. Every response's tokens are charged to the agent's budget, and the next
response is capped to what is left. Illegal or invalid calls go back to Claude as
errors, ending with the actions allowed now, so it can correct itself; after three
rejected calls in a row the run stops as stuck instead of looping. See examples/refund.py.
Running it with OpenAI
The same loop over the Responses API (pip install "concordat[openai]"):
import openai
from concordat.openai import run
result = run(RefundAgent(), openai.OpenAI(), "Please refund order A-100.", model="...")
Without classes
The same protocol, using the kernel directly:
from concordat import Budget, Protocol, Session
refund = Protocol(
"start",
{
"start": {"verify_identity": {"verified", "denied"}},
"verified": {"propose_refund": {"awaiting_approval"}},
"awaiting_approval": {"human.approve": {"refunded"}, "human.reject": {"denied"}},
},
final={"refunded", "denied"},
) # checked here: no dead ends, no unreachable states
session = Session(refund, Budget(tokens=50_000, tool_calls=10))
session.allowed_actions() # {'verify_identity'}: only these tools go to the model
session.step("propose_refund") # ProtocolViolation: not allowed in state 'start'
Handing work to another agent splits the budget, and the parent can't spend it twice:
child_budget = session.budget.split(tokens=10_000, tool_calls=3)
Development
uv sync
uv run pytest
uv run mypy src
uv run ruff check
License
Apache-2.0
Metadata
Release files for concordat 0.0.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| concordat-0.0.5.tar.gz | 221.8 kB | Without provenance | Details for concordat-0.0.5.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| concordat-0.0.5-py3-none-any.whl | Python 3 | none | any | Details for concordat-0.0.5-py3-none-any.whl |
Total release size: 242.8 kB
Release files / concordat-0.0.5.tar.gz
| Download URL | concordat-0.0.5.tar.gz |
|---|---|
| Size | 221.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
df2b4f81fa3f51a66e8bfcc401170f2e8c6a5bbab09479f6f056f7e7d3e7c6c7
|
|
BLAKE2b-256 checksum How to use checksums |
30010adc7b9187fe193c24b0cc30c8f046f030c5cf5279eb768bf2d38936fa55
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / concordat-0.0.5-py3-none-any.whl
| Download URL | concordat-0.0.5-py3-none-any.whl |
|---|---|
| Size | 21.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e0b8b194d1c1dc436db6af3029a42b65f58c93a57114f0d531229a07a6274f61
|
|
BLAKE2b-256 checksum How to use checksums |
1287494e595242716a7a10ede0de70155370d3febee2e1906cc179ffcd706150
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for concordat 0.0.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.