Skip to main content

counterpart

CI PyPI Python License

Test your A2A agent against mocks that check for correct answers, not just completed tasks.

If your agent delegates work to somebody else's agent, you cannot test it without them. counterpart stands in for those agents: ones that work, ones that stall, ones that come back with junk. It works in both directions, so you can be the caller or the one being called.

The reason it also reads the replies is that A2A only tells you a task finished. Finished and correct are different things, and only one of them is on the wire.

pip install counterpart
async def test_peer_lies_about_finishing(mock_agent):
    peer = mock_agent("false_success")     # reports done, returns garbage

    task = await peer.ask("Quote 2 pallets LA to Dallas", contract={"price": float})

    assert task.status == "completed"   # the peer said it finished
    assert task.contract_violated      # what it sent back was unusable

Installing the package is the whole setup. The mock_agent fixture and the async configuration come with it, so there is no conftest.py to write.

{"price": float} is shorthand for a contract that requires a price field which is a number. When you want more, build one properly. Pass a pydantic model, add named checks, and use .client() when a test needs several turns on one connection:

contract = (
    Contract("freight quote")
    .returns(Quote, strict=True)                       # a real model, no coercion
    .require(price_positive=lambda q: q.price > 0)
)

peer = mock_agent("clarifier", question="Deliver by when?")
async with peer.client() as client:                    # one connection, several turns
    task = await client.send_message("Quote 2 pallets", contract=contract)
    task = await client.reply(task, "Friday")

Why check the reply as well

Mocking the transport is the easy half. The reason counterpart also looks at content is that completed only tells you the other agent stopped working. Every one of these arrives as a completed task, and the protocol is satisfied by all of them:

{"message": "Your quote is ready!"}          # reads fine, contains no price
{"price": "call for rate"}                   # a sentence where you needed a number
{"price": 1420.0, "account": "AC-99812"}     # right shape, somebody else's account
{}                                           # nothing at all

Your code then carries on with whatever it got. Nothing raised, nothing was logged, and the bad value is now three functions downstream or in an invoice. That is what makes these expensive to find: you are debugging backwards from a wrong number, days later, with no stack trace to start from. One study of multi-agent failures attributes 45 to 79 percent of them to this rather than to anything crashing.

A contract is just you writing down what you were expecting, so the test fails at the point the peer lets you down instead of somewhere later.

Personas

Every persona is deterministic, needs no LLM, and drives a real A2A task lifecycle.

Persona What it does
cooperative Works and completes with a well formed result
clarifier Asks one question, waits for your answer, then completes
false_success Reports completed and returns garbage
resource_abuse Stalls or streams forever without finishing
flaky Drops the connection, then recovers on retry
over_sharing Asks for more context than the task needs

Writing your own takes one class and one call. There is no DSL.

from counterpart.core import Complete, Progress
from counterpart.personas import register

class HalfAnswer:
    def respond(self, turn, ctx):
        return [Progress("working"), Complete(result={"partial": True})]

register("half_answer", HalfAnswer)

One thing to know about contracts

By default returns() uses pydantic's normal mode, so a peer sending {"price": "1420.00"} gets coerced to a float and passes. Your predicates then see a real number, so an isinstance check will not save you. That is ordinary pydantic behaviour, but it surprises people here, because you might reasonably think you asked for type validation.

If you want the stricter reading, ask for it:

Contract("fare").returns(Fare, strict=True)

Lax is the default on purpose. Plenty of real services send numbers as strings, and a contract that rejects valid traffic gets switched off, which leaves you with nothing. Use strict=True when you control both ends or the format is pinned.

Testing the other direction

Everything above has your agent doing the calling. Sometimes you are the one being called, and you want to know your agent answers properly when somebody else's agent asks it something. The same mocks work as the caller.

wrap() turns any callable, sync or async, into an A2A server, and the mock sends it work:

from counterpart import MockAgent, wrap
from counterpart.adapters.a2a.mockagent import serve_asgi

with serve_asgi(wrap(my_agent, name="quoting-agent")) as url:
    peer = MockAgent("cooperative")
    task = await peer.send_task(url, "Quote 2 pallets", contract={"price": float})

assert not task.contract_violated      # your agent's own answer held up

The contract checks your agent this time, which is a quick way to catch your own service returning a completed task with nothing useful in it. Run the server with uvicorn instead if you want a long-lived one, or skip the socket with A2AClient(app=app).

Checking a live agent

counterpart check https://my-agent.example.com
counterpart attack https://my-agent.example.com --json

check gives you a scored report where every row cites the spec section it verifies. It is a quick smoke test for the development loop, not a certification suite. For the full conformance matrix use the official a2a-tck. attack sends adversarial probes and reports what came back.

Examples

There are four runnable scenarios in examples, each in a domain where the counterparty genuinely belongs to someone else, and each built around a different kind of failure.

Freight procurement collects quotes from competing carriers and picks the cheapest usable one. The naive version books the carrier whose completed quote lists price as "call for rate", and sends that string to invoicing.

Prior authorization is a clinic getting a procedure approved by a health insurer before performing it, which is a real cross-company agent problem with money and patient safety attached. It models 25 insurer responses that all report completed, taken from the healthcare messaging standards and from billing forums where people describe what actually goes wrong. Each is labelled by how well attested it is, and two are marked unattested because I could not find a real case for them. One of them is worth the click: the standard's own published example of a pending decision reports success, with the only honest signal buried in an optional nested field.

Satellite downlink schedules a pass with a ground station network. Twelve plans that all validate and all describe a different physical reality: a window handed over in GPS time, elevation in radians where the field says degrees, an inertial frame where you need an earth fixed one. This is the failure that cost NASA the Mars Climate Orbiter.

Chaos multihop is deliberately excessive. Four hops, each a separate HTTP server, auth passed along the chain, twenty concurrent users, and corruption injected at the deepest hop. Corruption three hops away still gets caught at the top.

Each one checks two things, and the second matters more. Every unusable answer has to be caught, and every legitimate answer has to be left alone.

That second half is where the real difficulty turned out to be. Going through the actual industry value sets found four bugs in the contracts I had written for these examples, and every single one was a false positive. My checks were rejecting good answers: an approval code I had not thought to accept, an id that came back correct but padded with spaces, and a date compared as text so that 07/31/2026 slipped past a check it should have failed. None of the four was a missed catch.

That is the trap with this kind of testing. Catching bad data is easy. A checker that also flags good data gets switched off, and then you are back to having none, so both examples test for false positives explicitly.

What it does not do

This is testing you run before you deploy. It does not watch production, it does not solve agent identity or trust, and it will not bound how deep a peer delegates. A contract only sees the payload, not the clock, so if a correct answer arriving 400ms late is useless to you, that timeout is yours to enforce. limits_probe.py shows exactly where the library goes quiet.

If your agents are three functions in the same process, you do not need any of this.

Design

The core has no protocol code in it. A2A is one adapter, and a test enforces the boundary, so the contract engine also works on a plain HTTP response, an MCP style tool result, or a function return.

  • docs/spec-notes.md maps A2A v1.0 to the code, citing the spec throughout. Wire types are checked against the vendored normative proto.
  • docs/prior-art.md covers what already exists and where this fits.
  • docs/roadmap.md lists what is out of scope and what is known to be missing.

Status

Version 0.1.3, built against A2A spec v1.0. The API will change. Apache-2.0.

If you have hit this failure in something you built, I would like to hear about it. Open an issue with the shape of the response that fooled you.

Metadata

Release files for counterpart 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for counterpart 0.1.3
File Size Uploaded
counterpart-0.1.3.tar.gz 150.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for counterpart 0.1.3
File Interpreter ABI Platform
counterpart-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 208.2 kB

Release files / counterpart-0.1.3.tar.gz

Download URL counterpart-0.1.3.tar.gz
Size 150.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5a31b5927d1ce809bf575f96c26abbbe2e0e85d961e0bd8c42a00d4d4547d105
BLAKE2b-256 checksum
How to use checksums
77efedd9741d1d1bc7090b260b7c015760994cbfbc1a6dd3e09793e85900cf44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release files / counterpart-0.1.3-py3-none-any.whl

Download URL counterpart-0.1.3-py3-none-any.whl
Size 58.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4d76f235dc9f2249d6a68005422a549e56e49cb312944764af7f472b7a017fd1
BLAKE2b-256 checksum
How to use checksums
5faa3720e2f587e50092b65a82b524e5751e6634eccb589661665efd491317f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

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