Metadata-Version: 2.5
Name: a2a-spec
Version: 0.3.2
Summary: The open specification for testing, validating, and guaranteeing agent-to-agent interactions.
Project-URL: Homepage, https://github.com/padobrik/a2a-spec
Project-URL: Documentation, https://github.com/padobrik/a2a-spec/tree/main/docs
Project-URL: Repository, https://github.com/padobrik/a2a-spec
Project-URL: Issues, https://github.com/padobrik/a2a-spec/issues
Author-email: Fedor Kabachenko <fkabachenko@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: a2a,agents,ai,contract-testing,llm,multi-agent,testing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: jsonschema<5.0,>=4.20
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: pyyaml<7.0,>=6.0
Requires-Dist: rich<16.0,>=13.0
Requires-Dist: typer<1.0,>=0.12
Provides-Extra: all
Requires-Dist: a2a-sdk[http-server]<2.0,>=1.0; extra == 'all'
Requires-Dist: langchain-core<2.0,>=0.2; extra == 'all'
Requires-Dist: mypy<3.0,>=1.10; extra == 'all'
Requires-Dist: numpy<3.0,>=1.26; extra == 'all'
Requires-Dist: pre-commit<5.0,>=3.7; extra == 'all'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'all'
Requires-Dist: pytest-cov<8.0,>=5.0; extra == 'all'
Requires-Dist: pytest<10.0,>=8.0; extra == 'all'
Requires-Dist: ruff<1.0,>=0.5; extra == 'all'
Requires-Dist: sentence-transformers<6.0,>=3.0; extra == 'all'
Requires-Dist: types-jsonschema>=4.20; extra == 'all'
Requires-Dist: types-pyyaml>=6.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: a2a-sdk[http-server]<2.0,>=1.0; extra == 'dev'
Requires-Dist: mypy<3.0,>=1.10; extra == 'dev'
Requires-Dist: pre-commit<5.0,>=3.7; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
Requires-Dist: pytest-cov<8.0,>=5.0; extra == 'dev'
Requires-Dist: pytest<10.0,>=8.0; extra == 'dev'
Requires-Dist: ruff<1.0,>=0.5; extra == 'dev'
Requires-Dist: types-jsonschema>=4.20; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain-core<2.0,>=0.2; extra == 'langchain'
Provides-Extra: semantic
Requires-Dist: numpy<3.0,>=1.26; extra == 'semantic'
Requires-Dist: sentence-transformers<6.0,>=3.0; extra == 'semantic'
Description-Content-Type: text/markdown

<p align="center">
  <strong>a2a-spec</strong><br>
  <em>Contract tests for multi-agent systems</em>
</p>

<p align="center">
  <a href="https://pypi.org/project/a2a-spec/"><img src="https://img.shields.io/pypi/v/a2a-spec?style=flat-square" alt="PyPI"></a>
  <a href="https://github.com/padobrik/a2a-spec/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/padobrik/a2a-spec/ci.yml?branch=main&style=flat-square&label=CI" alt="CI"></a>
  <a href="https://github.com/padobrik/a2a-spec/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square" alt="License"></a>
  <img src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue?style=flat-square" alt="Python 3.11+">
</p>

<p align="center">
  <img src="https://raw.githubusercontent.com/padobrik/a2a-spec/main/docs/demo.gif" alt="a2aspec infer generates a contract from the consumer's code; after a producer change, the hand-written spec passes and the inferred contract fails" width="760">
</p>

a2a-spec finds the fields that one agent reads from another agent. It tests these fields in CI. The tests use recorded outputs, so they make no LLM calls and need no API keys.

## The problem

Agent A sends data to agent B. You change the prompt of agent A, and its output changes. For example, agent A now sends `urgency` instead of `priority`.

Agent B reads `priority` with a default value. It does not stop with an error. It gives a wrong result, and no test fails.

## What a2a-spec does

1. It reads the code of agent B and finds the fields that agent B uses.
2. It records the output of agent A one time and saves it as JSON files in git.
3. It tests the recorded output against the fields that agent B needs. You run these tests in pytest or in CI.

<p align="center">
  <img src="https://raw.githubusercontent.com/padobrik/a2a-spec/main/docs/diagrams/how-it-works.svg" alt="a2aspec record saves the output of agent A as snapshots. a2aspec infer reads the code of agent B and writes a spec. a2aspec test or pytest tests the snapshots against the spec, with no LLM calls, and gives pass or fail." width="760">
</p>

## Terms

| Term | Meaning |
|---|---|
| Producer | The agent that sends the data. |
| Consumer | The agent that receives the data. |
| Spec | A YAML file. It gives the fields that the consumer needs from the producer. |
| Snapshot | A JSON file. It contains one recorded input and output of the producer. |
| Scenario | A named input for the producer. |
| Adapter | A small Python object. a2a-spec uses it to call your agent. |

## Get started

You need Python 3.11 or later and a git repository.

This example uses two agents. The producer `triage_agent` classifies a message. The consumer `resolution_agent` uses that result.

```python
# agents/triage.py
async def triage_agent(data: dict) -> dict:
    text = data["message"].lower()
    category = "billing" if "charge" in text else "general"
    return {"category": category, "priority": "high" if "urgent" in text else "low"}
```

```python
# agents/resolution.py
async def resolution_agent(ticket: dict) -> dict:
    escalate = ticket.get("priority", "low") == "high"
    return {"reply": f"We are checking your {ticket['category']} issue.", "escalate": escalate}
```

**1. Install a2a-spec.**

```bash
pip install a2a-spec
```

**2. Create the project files.**

```bash
a2aspec init
```

This command creates `a2a-spec.yaml` and the `a2a_spec/` directory.

**3. Remove the example spec and the example scenario.**

```bash
rm a2a_spec/specs/example-spec.yaml a2a_spec/scenarios/example-scenarios.yaml
```

**4. Add the producer to `a2a_spec/adapters/__init__.py`.**

```python
from a2a_spec import FunctionAdapter
from agents.triage import triage_agent

ADAPTERS = {
    "triage-agent": FunctionAdapter(fn=triage_agent, agent_id="triage-agent", version="1.0"),
}
```

**5. Add inputs for the producer to `a2a_spec/scenarios/support.yaml`.**

```yaml
scenarios:
  - name: double_charge
    input:
      message: "I was charged twice. This is urgent."
  - name: question
    input:
      message: "How do I change my address?"
```

**6. Record the output of the producer.**

```bash
a2aspec record
```

The command calls the producer one time for each scenario:

```
    PASS triage-agent / double_charge recorded
    PASS triage-agent / question recorded
[done] 2 snapshots saved to ./a2a_spec/snapshots
```

**7. Make the spec from the code of the consumer.**

```bash
a2aspec infer --producer triage-agent --consumer resolution-agent \
  --source agents/resolution.py:resolution_agent
```

The command writes `a2a_spec/specs/triage-agent-to-resolution-agent.yaml`:

```yaml
# Generated by 'a2aspec infer' from agents/resolution.py:resolution_agent and 2 triage-agent snapshot(s). Review before committing.
#   priority: required, read at line 2
#   category: required, read at line 3
spec:
  name: triage-agent-to-resolution-agent
  producer: triage-agent
  consumer: resolution-agent
  structural:
    type: object
    required: [priority, category]
    properties:
      priority: {type: string}
      category: {type: string}
```

Read the spec before you commit it. If the command shows a `WARN` line, read [Limits](#limits).

**8. Run the tests.**

```bash
a2aspec test
```

```
PASS triage-agent-to-resolution-agent / double_charge
PASS triage-agent-to-resolution-agent / question
  2 passed  (total: 2)
```

**9. Commit the files.**

```bash
git add a2a-spec.yaml a2a_spec agents
git commit -m "test: add agent contracts"
```

The committed snapshots are the baseline for the next steps.

## When you change an agent

<p align="center">
  <img src="https://raw.githubusercontent.com/padobrik/a2a-spec/main/docs/diagrams/change-workflow.svg" alt="Change the producer, then run a2aspec record, a2aspec diff and a2aspec test. If the tests pass, commit. If they fail and the change is not intended, fix the producer and record again. If the change is intended, update the consumer, run a2aspec infer --force and commit." width="760">
</p>

**1. Change the producer.** For example, change its prompt, its model or its code. In this example, the producer now sends `urgency` instead of `priority`.

**2. Record the output again.**

```bash
a2aspec record
```

**3. Compare the new output with the committed snapshots.**

```bash
a2aspec diff
```

```
  triage-agent / double_charge:
    FAIL priority: Field 'priority' was removed
    FAIL urgency: Field 'urgency' was added
```

**4. Run the tests.**

```bash
a2aspec test
```

```
FAIL triage-agent-to-resolution-agent / double_charge
    [<root>] 'priority' is a required property
```

**5. Correct the problem.**

- If the change in the producer is an error, correct the producer. Then do steps 2 to 4 again.
- If the change is necessary, change the consumer. Then run `a2aspec infer` with `--force` and commit the new spec and snapshots.

Do not record the snapshots again only to make a test pass. Do not delete snapshots or remove fields from a spec for this purpose.

## Run the tests with pytest

a2a-spec has a pytest plugin. Add these lines to `pyproject.toml`:

```toml
[tool.pytest.ini_options]
a2a_config = "a2a-spec.yaml"
testpaths = ["tests", "a2a_spec/specs"]
```

pytest then runs one test for each snapshot:

```
$ pytest -v
a2a_spec/specs/triage-agent-to-resolution-agent.yaml::double_charge PASSED   [ 50%]
a2a_spec/specs/triage-agent-to-resolution-agent.yaml::question PASSED        [100%]
```

The usual pytest options work, for example `-k`, `-x` and `--junitxml`. To run the tests one time without the configuration, use `pytest --a2a`.

To test a live output in your own test, use the `a2a_check` fixture. This example is an `async` test, so it needs an async plugin such as `pytest-asyncio`.

```python
async def test_triage_live(a2a_check):
    output = await triage_agent({"message": "I was charged twice"})
    a2a_check("triage-agent-to-resolution-agent", output)
```

## Run the tests in CI

The tests use the committed snapshots. CI needs no API keys.

```yaml
# .github/workflows/contracts.yml
name: Agent contracts
on: [push, pull_request]

jobs:
  contracts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - run: pip install a2a-spec
      - run: a2aspec test
```

If your specs have semantic rules, install `a2a-spec[semantic]` instead.

## Write a spec by hand

A spec can have three types of rules:

| Rule type | What it finds | How |
|---|---|---|
| `structural` | A missing field, a wrong type, a value out of range | JSON Schema |
| `semantic` | A text field that no longer means the same thing | Similarity of local embeddings |
| `policy` | Text that must not occur, for example a card number | Regular expressions or your own function |

```yaml
spec:
  name: triage-to-resolution
  version: "1.0"
  producer: triage-agent
  consumer: resolution-agent
  structural:
    type: object
    required: [category]
    properties:
      category: {type: string, enum: [billing, general]}
  semantic:
    - rule: summary_matches_message
      method: embedding_similarity
      source_field: message     # field in the producer input
      target_field: summary     # field in the producer output
      threshold: 0.5
  policy:
    - rule: no_card_numbers
      method: regex
      patterns: ['\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b']
```

Semantic rules need `pip install "a2a-spec[semantic]"`. See [Writing specs](docs/writing-specs.md) for all options.

## Connect your agents

| Agent type | Adapter |
|---|---|
| Python `async` function | `FunctionAdapter(fn=..., agent_id=..., version=...)` |
| HTTP endpoint (POST JSON, receive JSON) | `HTTPAdapter(url=..., agent_id=..., version=...)` |
| LangChain runnable | `LangChainAdapter(runnable=..., agent_id=..., version=...)` (install `a2a-spec[langchain]`) |
| Agent with the [A2A protocol](https://a2a-protocol.org) 1.0 | `A2AAdapter(url=..., agent_id=...)` |

For an A2A agent, give the base URL of the agent. The adapter reads the Agent Card, finds the JSON-RPC interface and sends the scenario input. It records the agent version from the Agent Card in each snapshot.

```python
from a2a_spec import A2AAdapter

ADAPTERS = {
    "triage-agent": A2AAdapter(url="https://triage.example.com", agent_id="triage-agent"),
}
```

See [Writing adapters](docs/writing-adapters.md) for all options.

## Use with coding agents

a2a-spec contains an [Agent Skill](https://agentskills.io). The skill tells a coding agent how to use a2a-spec. It also tells the agent not to make a failed test pass by recording the snapshots again.

```bash
a2aspec skill --dir path/to/skills
```

This command writes `path/to/skills/a2a-spec/SKILL.md`. Export the skill again after you update a2a-spec.

## Limits

- `a2aspec infer` reads Python consumers only. It finds `data["key"]`, `data.get("key")` and `"key" in data` for top-level keys.
- If the consumer gives its input to another function, `a2aspec infer` cannot follow it. The command then shows a `WARN` line. Add the missing fields to the spec by hand.
- `a2aspec diff` compares with the snapshots in git. Commit the snapshots before you use it.
- `A2AAdapter` supports A2A 1.0 with JSON-RPC. It does not support streaming or file parts.
- Semantic rules use a local embedding model. The `semantic` extra installs PyTorch.

## Commands

| Command | What it does |
|---|---|
| `a2aspec init` | Creates the project files. |
| `a2aspec record` | Calls the producers and saves their output as snapshots. |
| `a2aspec infer` | Makes a spec from the fields that a consumer reads. |
| `a2aspec test` | Tests the snapshots against the specs. |
| `a2aspec diff` | Compares the snapshots with the committed snapshots. |
| `a2aspec skill` | Exports the skill for coding agents. |

See [CLI reference](docs/cli-reference.md) for all options.

## Documentation

- [Writing specs](docs/writing-specs.md)
- [Writing adapters](docs/writing-adapters.md)
- [CLI reference](docs/cli-reference.md)
- [CI integration](docs/ci-integration.md)
- [Concepts](docs/concepts.md)
- [Architecture](docs/architecture.md)
- [Example project](examples/customer_support/)

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

Apache 2.0. See [LICENSE](LICENSE).
