Skip to main content

counterpart

CI PyPI Python License


A2A lets one AI agent hand work to another. counterpart plays the agent on the other side, from helpful to hostile, and catches the replies your code should never accept:

async def test_carrier_reports_success_with_no_price(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

Why counterpart

The agents your agent depends on belong to other people. You cannot spin up a carrier's agent or an insurer's agent to test against, so counterpart stands in for them. And it reads what they send back, because completed only means the other agent stopped working. Every one of these arrives as a completed task with the protocol fully satisfied:

{"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

A contract is you writing down what you were expecting, so the test fails at the point the peer lets you down instead of three functions later or in an invoice. {"price": float} is the shorthand form. For real checks, pass a pydantic model and add named predicates, with strict=True when you do not want "1420.00" silently coerced to a number:

contract = (
    Contract("freight quote")
    .returns(Quote, strict=True)
    .require(price_positive=lambda q: q.price > 0)
)

The mock runs inside your test process. No socket, no port, nothing on the network. mock_agent is the pytest fixture; MockAgent is the same object without pytest, and its .serve() binds a real address when you need to point another process at it.

It also works in the other direction, with the mock as the caller and your agent answering. wrap() turns any function into an A2A server, and the same contract now judges your answers:

from counterpart import MockAgent, serve_asgi, wrap

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 own answer held up

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

Personas

Six ship with the package. All deterministic, none needing an LLM.

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)

Installation

pip install counterpart

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

There is also a CLI for poking a live agent: counterpart check <url> scores it against the spec, section by section, and counterpart attack <url> sends deliberately malformed requests and reports what came back. For the full rulebook use the official a2a-tck.

Examples

Five runnable scenarios in examples, each in a domain where the counterparty genuinely belongs to somebody else:

  • Freight procurement: pick the cheapest usable quote, not the cheapest string that parses.
  • Freight edge cases: 22 ways a quote is unusable, 14 that look wrong and are fine. Checking them changes which carrier gets booked, at $592.50 more.
  • Prior authorization: 25 insurer replies that all report success, including the standard's own example of a pending decision that does.
  • Satellite downlink: twelve plans that all validate and each describes a different physical reality. Radians where the field says degrees.
  • Chaos multihop: four hops, real sockets, twenty concurrent users, corruption injected at the bottom and caught at the top.

Every example tests both halves: each unusable answer is caught, and each legitimate answer is left alone. The second half is harder. All four bugs found in these contracts were false positives, checks rejecting good answers, and a checker that flags good data gets switched off.

Documentation

Contributing

Version 0.1.13, Apache-2.0, the API will change. If a peer's reply has fooled something you built, open an issue with the shape of the payload. That is the most useful contribution this project can get.

Metadata

Release files for counterpart 0.1.13

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.13
File Size Uploaded
counterpart-0.1.13.tar.gz 143.5 kB Details

Built distribution (wheel)

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

Total release size: 199.9 kB

Release files / counterpart-0.1.13.tar.gz

Download URL counterpart-0.1.13.tar.gz
Size 143.5 kB
Tags Source
SHA-256 checksum
How to use checksums
fda4250dd937f3c39ba320cc4c278eba294a7aefc6fc5c9c71d0883a7bc50ee6
BLAKE2b-256 checksum
How to use checksums
d26b70195a22438da507149e0486892e69d47df4aa2a518d5c6fc0fc3e954d65
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.13-py3-none-any.whl

Download URL counterpart-0.1.13-py3-none-any.whl
Size 56.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a4a8693e7e14980987eb34037e1d88a700fa95e646fc7b67542ea2cc4a019d9
BLAKE2b-256 checksum
How to use checksums
bf041de9156f2fa895171fbd79fe128b9e58253f7070eab6754853107d0332ed
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

This release

0.1.13 This release

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

0.1.3

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