🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: a2aproject/a2a-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.1.5
Choose a base ref
...
head repository: a2aproject/a2a-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.2.0
Choose a head ref
  • 12 commits
  • 60 files changed
  • 7 contributors

Commits on Sep 21, 2026

  1. ci: auto-assign triage owner on new issues (#1264)

    Assigns a triage owner automatically when an issue is opened, so new
    reports surface immediately instead of waiting to be noticed.
    
    __Tested:__
    Verified end to end on a fork: the run triggers, the assignee is set,
    and the issue notification is delivered.
    mykytanetipa authored Sep 21, 2026
    Configuration menu
    Copy the full SHA
    a4c68f6 View commit details
    Browse the repository at this point in the history
  2. docs(samples): add Agent Card signing sample (#1198)

    There was no runnable example for `a2a.utils.signing`, so this adds one
    covering both halves of Agent Card signing.
    
    `samples/agent_card_signing.py`:
    
    - **Server**: generates an ES256 key, signs the Agent Card with
      `create_agent_card_signer`, serves it via
    `create_agent_card_routes(card_modifier=...)`, and publishes the public
    key
    as a JWKS document at `/.well-known/jwks.json` — the URL the signature's
      `jku` points to.
    - **Client**: verifies the card with `create_signature_verifier` passed
    to
    `A2ACardResolver.get_agent_card(signature_verifier=...)`, resolving keys
    by
      `kid` from an allowlist of pinned JWKS URLs, with algorithms pinned to
      `ES256`.
    - **Modes**: `demo` (default, runs both in-process), `serve`, `verify
    --url`.
    
    The default `demo` mode verifies the genuine card and then shows the
    three
    rejections a verifier exists for:
    
    ```
    1. The card served by the agent
       verified card for agent: Signed Card Agent
       protected header: {'alg': 'ES256', 'jku': '.../.well-known/jwks.json', 'kid': 'sample-card-key-1', 'typ': 'JOSE'}
    
    2. A card whose transport URL was rewritten in transit
       rejected as expected: No valid signature found
    
    3. A card with its signature stripped off
       rejected as expected: AgentCard has no signatures to verify.
    
    4. The genuine card, checked by a client that pins other keys
       rejected as expected: No valid signature found
    ```
    
    Two non-obvious details are called out in comments, since they bite
    anyone
    copying the pattern:
    
    - The verifier is a **synchronous** callable, so fetching the JWKS
    inside it
      with a sync HTTP client blocks the event loop (it deadlocks against an
    in-process server and reports `No valid signature found`). The key
    provider
      does pure in-memory lookups over JWKS prefetched asynchronously.
    - `create_agent_card_signer` **mutates** the card it signs, so a
    per-request
    `card_modifier` would accumulate signatures. The helper deep-copies
    first and
      the card is signed once at startup.
    
    The key provider also raises `PyJWKError` rather than a plain exception,
    so the
    verifier treats one unusable signature as unverifiable and moves to the
    next
    instead of aborting a card that also carries a good signature.
    
    Also adds `tests/integration/test_agent_card_signing_smoke.py` (runs the
    demo
    on a free port, asserts one acceptance and three rejections) and a
    `samples/README.md` section.
    
    No library code changed — samples, tests and docs only.
    
    ## Testing
    
    - `uv run pytest tests/utils/test_signing.py
    tests/integration/test_agent_card_signing_smoke.py
    tests/client/test_card_resolver.py` → 55 passed
    - `uv run ruff format --check` and `uv run ruff check` → clean
    - `uv run ty check` → same 4 pre-existing diagnostics as `main` (samples
    are outside `[tool.ty.src]`)
    
    ## Checklist
    
    - [x] Follow the [`CONTRIBUTING`
    Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md).
    - [x] Make your Pull Request title in the
    <https://www.conventionalcommits.org/> specification.
    - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh`
    from the repository root to format)
    - [x] Appropriate docs were updated (if necessary)
    
    Fixes #1197 🦕
    
    ---------
    
    Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    3 people authored Sep 21, 2026
    Configuration menu
    Copy the full SHA
    f3ac824 View commit details
    Browse the repository at this point in the history

Commits on Sep 24, 2026

  1. fix: compare in-memory task timestamps numerically (#1233)

    # Description
    
    Fix timestamp filtering and descending ordering in
    `InMemoryTaskStore.list()`. Comparing `ToJsonString()` values
    lexicographically reverses chronological order when fractional-second
    precision differs. For example, `2025-01-01T00:00:00.123Z` incorrectly
    passes a filter whose threshold is `2025-01-01T00:00:00.123000001Z`,
    despite being one nanosecond earlier. The same comparison also produces
    incorrectly ordered pages.
    
    Compare integer nanoseconds using `ToNanoseconds()` for both filtering
    and sorting. Preserve the inclusive filter boundary, descending ID
    tie-breaker, and placement of tasks without timestamps after timestamped
    tasks.
    
    [A2A specification §5.6.1,
    Timestamps](https://a2a-protocol.org/latest/specification/#561-timestamps)
    states: “Millisecond precision SHOULD be used where available”. It also
    specifies `google.protobuf.Timestamp`, whose representation contains
    seconds and nanoseconds, and permits omitted fractional seconds. Numeric
    comparison handles these precisions without discarding sub-millisecond
    information.
    
    Add 15 regression cases covering seconds, milliseconds, microseconds,
    nanoseconds, equal timestamps, second boundaries, pagination, ID
    tie-breaking, absent timestamps/status, and timestamps before/at the
    Unix epoch. Before the fix, 11 cases fail and four pass; after the fix,
    all 35 in-memory task store tests pass.
    
    Validation on Python 3.10 with PostgreSQL 15 and MySQL 8:
    
    - `./scripts/lint.sh`: Ruff check/format and ty passed (three existing
    unused-ignore warnings outside this change).
    - `uv run pytest`: 2,092 passed, 3 xfailed, 1 xpassed. The xfail/xpass
    cases are already marked for #869.
    - `uv run pytest --cov=src --cov-report=term-missing
    --cov-fail-under=88`: same test results, 93.17% coverage.
    - JSCPD: 2.61% duplication, below the 3% threshold.
    - Built the wheel and ran `tests.install_smoke` in six clean
    environments: base, http-server, fastapi, grpc, telemetry, and sql all
    passed.
    
    Local transport checks used proxy exclusions for loopback addresses.
    Cross-version `uv --with` environments were prepared before the full
    runs to keep dependency downloads outside server-startup timeouts.
    
    - [x] Follow the CONTRIBUTING guide: feature branch based on current
    upstream main.
    - [x] Conventional Commit title: `fix: compare in-memory task timestamps
    numerically`.
    - [x] Tests and linters pass.
    - [x] Protocol reference included above; no public API changes.
    
    Co-authored-by: Gatsuk Andrey <GatsukAS@zdrav.mos.ru>
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    3 people authored Sep 24, 2026
    Configuration menu
    Copy the full SHA
    0d5473c View commit details
    Browse the repository at this point in the history

Commits on Sep 29, 2026

  1. fix(server): reject terminal-task operations with UnsupportedOperatio…

    …nError (#1268)
    
    ## What
    
    Spec
    [3.1.1](https://a2a-protocol.org/latest/specification/#311-send-message)
    names `UnsupportedOperationError` for "Messages sent to Tasks that are
    in a terminal state", and 3.3.2 repeats it for any operation on a
    terminal task. 5.4 maps that to `-32004` / `FAILED_PRECONDITION` / HTTP
    400. `ActiveTask` raised `InvalidParamsError` (`-32602`) instead, at all
    three of its terminal-state guards.
    
    That covers both `send_message` and `subscribe_to_task`, because
    `DefaultRequestHandlerV2.on_subscribe_to_task` has no terminal check of
    its own: it calls `get_or_create`, which calls `ActiveTask.start()`, and
    the error surfaces out of the first `__anext__`.
    
    Worth knowing while reading: `DefaultRequestHandler` is an alias for
    `DefaultRequestHandlerV2` (`request_handlers/__init__.py:46`), so
    `default_request_handler.py` is the legacy handler and is not on the
    default path. It already answered subscribe correctly at line 613, which
    is why only the v2 path drifted. Its send path is corrected here too, so
    the two handlers and the two operations finally agree.
    
    ## Cancel keeps its own error
    
    `on_cancel_task` deliberately catches `ActiveTask`'s terminal-state
    error and re-raises `TaskNotCancelableError`, which 3.3.2 gives its own
    entry. That remap is retained, now keyed on the new error type. Two ACTS
    tests (`CORE-CANCEL-002`, `CORE-ERR-002`) cover it and still pass.
    
    ## Tests
    
    `test_on_subscribe_to_task_in_terminal_state` is new for v2. Its absence
    is why this survived: the legacy suite had the equivalent test and
    asserted the correct error all along, so the divergence never showed up.
    Mutation-checked - reverting `active_task.py:472` alone fails all four
    parametrized cases.
    
    An existing `# TODO: Is it correct error code ?` in `test_scenarios.py`
    sat next to this exact assertion. It is now answered rather than carried
    forward.
    
    ## Result
    
    Clears `CORE-SEND-002`, `CORE-ERR-004` and `STREAM-SUB-003` (all `must`)
    on jsonrpc, grpc and rest - 9 of the 21 failing MUST test-instances.
    
    | binding | must before | must after |
    | --- | --- | --- |
    | jsonrpc | 52/59 | 55/59 |
    | grpc | 40/47 | 43/47 |
    | rest | 42/49 | 45/49 |
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    6cce91b View commit details
    Browse the repository at this point in the history
  2. feat(server): add opt-in validation of message media types against th…

    …e agent card (#1269)
    
    ## What
    
    Spec
    [3.3.2](https://a2a-protocol.org/latest/specification/#332-error-types)
    defines `ContentTypeNotSupportedError` for "A Media Type provided in the
    request's message parts ... is not supported by the agent".
    `ContentTypeNotSupportedError` was fully wired already - `-32005`,
    `INVALID_ARGUMENT`, HTTP 400, gRPC `INVALID_ARGUMENT` - but nothing ever
    read `AgentCard.default_input_modes`, so an undeclared `mediaType` was
    always accepted.
    
    This is the same change a2a-js made in
    [a2aproject/a2a-js#745](a2aproject/a2a-js#745),
    and it lands in the same place: a shared helper called from the one
    setup path that both `send_message` and `send_message_stream` go
    through, in each handler.
    
    ## Why opt-in
    
    Always-on would start refusing traffic these agents accept today. Four
    cards in this repo declare input modes their clients contradict:
    
    | card | declares |
    | --- | --- |
    | `samples/hello_world_agent.py:156` | `['text']` |
    | `samples/agent_card_signing.py:146` | `['text']` |
    | `itk/main.py:720` | `['text/plain']` |
    | `itk/acts_client_parse.py:123` | `['text/plain']` |
    
    `'text'` is not a media type, so a client sending `text/plain` against
    it would be rejected. a2a-js hit exactly this and reached the same
    conclusion. The ITK fixture opts in, since its card states its modes
    accurately.
    
    ## Two carve-outs
    
    A card declaring **no** input modes accepts everything - an empty
    `default_input_modes` is an absent declaration, not an empty allowlist.
    
    A part with **no** `media_type` is not checked. It is a proto3 string,
    so an absent one arrives as `''`; only a media type the client actually
    stated can contradict the card.
    
    ## Tests
    
    Six unit tests on the validator, three on the handler wiring, two on the
    legacy handler. The handler tests matter more than they look: a flag
    that is stored but never read is the failure mode here, so both states
    are asserted and both were mutation-checked - forcing the branch off
    fails the two rejection tests, forcing it on fails the opt-out test.
    
    ## Result
    
    Clears `CORE-SEND-004` (`must`) on all three bindings.
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    8037b24 View commit details
    Browse the repository at this point in the history
  3. fix(server): reject a message whose contextId disagrees with its task (…

    …#1270)
    
    ## What
    
    Spec
    [3.4.3](https://a2a-protocol.org/latest/specification/#343-multi-turn-conversation-patterns):
    "Agents **MUST** reject messages containing mismatching `contextId` and
    `taskId` (i.e., the provided `contextId` is different from that of the
    referenced `Task`)."
    
    A follow-up carrying a real `taskId` with someone else's `contextId` was
    accepted with HTTP 200, and the client's `contextId` was then written
    into the task's history while the `Task` kept its own - two disagreeing
    context ids inside one task.
    
    ## This guard already existed
    
    `RequestContext` has carried it all along
    (`agent_execution/context.py:77-78`). It only fires when a non-`None`
    `task` reaches `build()`, and v2 passes `task=None` on purpose:
    
    ```python
    # We will get the task when we have to process the request to avoid concurrent read/write issues.
    task=None,
    ```
    
    So the check went dead when V2 became the default. The legacy handler
    passes the task and still rejects.
    
    Rather than undo that deliberate `task=None`, the comparison goes where
    v2 already fetches the task to prove it exists, one line after the
    `TaskNotFoundError` it raises there. No extra read, and the task is not
    retained.
    
    ## Scoped to requests that name a task
    
    The check only runs under `if original_task_id:`. A `contextId` with no
    `taskId` is a new task in an existing context, which stays legal -
    `test_scenario_context_id_visibility` depends on it, and there is a test
    here pinning it.
    
    ## A test that passed for the wrong reason
    
    Worth flagging for review. `TaskManager` already rejects this mismatch
    *once the agent emits an event*, with `Context in event doesn't match
    TaskManager wrong-context : real-context`. That message happens to
    contain both context ids, so the obvious assertion -
    `pytest.raises(InvalidParamsError)` plus both ids in the message -
    passes with or without this change. It did, and mutation testing is what
    caught it.
    
    The test now asserts the executor was never awaited. That is the actual
    behavioural difference: the request is refused before any agent work
    starts, instead of after the agent has already been handed a request
    whose ids contradict each other.
    
    ## Result
    
    Clears `CORE-MULTI-006` (`must`) on all three bindings.
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    c25022f View commit details
    Browse the repository at this point in the history
  4. fix(server): send PushNotificationConfig.authentication as an Authori…

    …zation header (#1271)
    
    ## What
    
    Spec
    [4.3.3](https://a2a-protocol.org/latest/specification/#433-push-notification-payload)
    gives the webhook request an `Authorization: {authentication_scheme}
    {credentials}` header, built from
    `PushNotificationConfig.authentication`.
    
    `BasePushNotificationSender` never read that field - the only header it
    has ever sent is `X-A2A-Notification-Token`, from the sibling `token`. A
    receiver therefore had no credential to check and no way to tell a
    genuine push from a forged one, which is the whole point of registering
    `authentication` in the first place.
    
    Nothing suggests this was left to the application. The constructor
    docstring explicitly justifies delegating URL screening, on the grounds
    that the spec makes it a SHOULD, and says nothing about auth, which the
    spec makes a MUST. The code already reads the field next to it. This
    reads as an omission.
    
    ## Only when both halves are present
    
    `AuthenticationInfo` is a message, so a config can set `scheme` without
    `credentials`. Sending on that would put `Bearer ` with nothing after it
    on the wire - a header that looks like a credential and is not. Both
    must be non-empty.
    
    Note the singular `scheme`. Only the v0.3 compat type has repeated
    `schemes`, and `conversions.py:252-261` collapses it to `schemes[0]` on
    the way in, so the sender only ever deals with one.
    
    ## Heads-up on the ACTS side
    
    `SEC-PUSH-001`, `SEC-PUSH-002` and `PUSH-DELIV-001` currently pass in
    nightly CI for every SDK, so this may look like a fix to something that
    was not broken. It is not: the assertion that inspects the delivered
    request is new, added in
    [a2aproject/A2A@82c277f](a2aproject/A2A@82c277f).
    The corpus CI runs predates it, and the older tests only checked that
    the config API accepted the config - which, as that commit's own note
    puts it, "a server that stores it and pushes anonymously, or never
    pushes, satisfies".
    
    A consequence worth stating: no other SDK has been measured against the
    real assertion yet, so unlike the rest of this stack there is no
    cross-SDK corroboration here. The code is the evidence -
    `push_info.authentication` appears nowhere under `src/a2a/server/`
    before this change.
    
    ## Result
    
    Clears `SEC-PUSH-001`, `SEC-PUSH-002` (`must`) and `PUSH-DELIV-001`
    (`may`) on all three bindings, and with them the **last failing MUST**:
    
    | binding | must | verdict |
    | --- | --- | --- |
    | jsonrpc | 59/59 | CONFORMANT |
    | grpc | 47/47 | CONFORMANT |
    | rest | 49/49 | CONFORMANT |
    
    The three PRs after this one are SHOULD-level and change the verdict for
    nobody; they take the remaining non-MUST failures to zero.
    
    Closes #585
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    5751d31 View commit details
    Browse the repository at this point in the history
  5. feat(server): add caching headers to the agent card endpoint (#1272)

    ## What
    
    Spec
    [8.6.1](https://a2a-protocol.org/latest/specification/#861-server-requirements)
    asks the card endpoint for a `Cache-Control` header with a `max-age`,
    and an `ETag` "derived from the Agent Card's `version` field or a hash
    of the card content". It sent neither, so every client refetched the
    whole card every time with no way to revalidate - for content the spec
    itself describes as changing "infrequently relative to the frequency at
    which clients may fetch it".
    
    One serving site, `agent_card_routes.py`. FastAPI re-wraps the same
    endpoint rather than building its own response, so there is no second
    variant to keep in step.
    
    ## Hash rather than version
    
    8.6.1 allows either, but `card_modifier` can return a different card per
    request without touching `version`, and a validator that goes stale
    silently is worse than none. Hashing the card actually being served
    stays correct under it. There is a test for exactly that.
    
    `json.dumps(sort_keys=True)` rather than the RFC 8785 canonicalization
    this repo already has for signing: an ETag is opaque and is only ever
    compared against one this server produced, so determinism is the entire
    requirement, and JCS would add a depth limit and a failure mode for
    nothing. The signing canonicalizer also strips `signatures`, which would
    be wrong here - a card whose signatures changed is a different response
    body.
    
    ## Conditional requests
    
    `If-None-Match` is honoured with a 304. Emitting a validator and then
    ignoring the header that uses it would leave clients re-downloading the
    card anyway, which is the thing 8.6 exists to avoid. Handled per RFC
    9110 8.2.2: comparison is weak, so `W/"x"` matches `"x"`, `*` matches,
    and a list is accepted.
    
    ## Cache-Control is opt-in
    
    8.6.1 ties `max-age` to "the agent's expected update frequency", which
    the SDK cannot know - a card regenerated per deployment and one static
    for months want very different numbers, and a wrong guess baked into the
    library is a stale card nobody can explain. It is a parameter with no
    default. The `ETag` ships either way, so the endpoint satisfies 8.6.1's
    either/or unconfigured, and revalidation costs one 304.
    
    ## Result
    
    Clears `CARD-CACHE-001` (`should`) on all three bindings, and takes grpc
    to zero failures at every level.
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    83d7f5d View commit details
    Browse the repository at this point in the history
  6. fix(server): ignore unrecognized request fields and fix parse-error d…

    …ata shape (#1273)
    
    Two problems on the same code path.
    
    ## Unrecognized fields
    
    Spec
    [5.7](https://a2a-protocol.org/latest/specification/#57-field-presence-and-optionality):
    "Implementations **SHOULD** ignore unrecognized fields in messages,
    allowing for forward compatibility as the protocol evolves."
    
    Both dispatchers parsed strictly, so `ignore_unknown_fields` defaulted
    to `False` and a client one minor version ahead was rejected outright
    over a field this build has never heard of. Four call sites, one flag.
    The v0.3 compat handler was already lenient, so this brings 1.0 into
    line with it.
    
    ## The flag also covers enum values
    
    `ignore_unknown_fields` is not only about field names: protobuf applies
    the same rule one level down and accepts an unknown enum value,
    defaulting it to 0. A future `ROLE_SYSTEM` now arrives as
    `ROLE_UNSPECIFIED` instead of failing the request.
    
    That is the same forward-compatibility bargain and arguably what 5.7 is
    for, but it is a real widening and protobuf offers no way to take the
    field leniency without it. It is commented at the call site and pinned
    by a test, so it is a decision on the record rather than something to be
    rediscovered later. **Reviewers: this is the part worth a second
    opinion.**
    
    ## error.data was a bare string
    
    Spec
    [9.5](https://a2a-protocol.org/latest/specification/#95-json-rpc-error-handling)
    makes `error.data` an "array of objects, each containing a `@type` key",
    and every object in it "**MUST** include a `@type` key".
    
    Only the `A2AError` path runs `build_error_details`, which produces that
    array. The parse-failure branch constructed the pydantic JSON-RPC model
    instead, whose `data` is `Any`, so the raw ProtoJSON diagnostic went out
    as a bare string. Both parse-failure sites now raise the `A2AError`, and
    the diagnostic survives in `ErrorInfo.metadata` rather than being
    dropped.
    
    No ACTS test reaches this - the corpus cannot construct a request that
    fails ProtoJSON parsing and then inspect `data` - so it is a genuine
    MUST violation that conformance would not have caught either way.
    
    ## Three tests moved off payloads that leniency now accepts
    
    `{'message': 'should be an object'}` and `{'message': {'text':
    'Hello'}}` both parse to an empty `Message` now, so they no longer stop
    at the parser. All three tests use mock handlers, which is why they saw
    it: they pin the parser layer with nothing behind it.
    
    Against a **real** handler, both payloads still answer `-32602`, from
    required-field validation one layer down. Verified before changing them:
    
    ```
    message as string  -> error -32602 Validation failed
    unknown key only   -> error -32602 Validation failed
    valid + unknown    -> OK
    ```
    
    So the end-to-end contract is unchanged; what moved is which layer
    rejects. The tests now use a value of the wrong shape (`parts` as a
    string), which leniency does not excuse, and each carries a comment
    saying why.
    
    ## Result
    
    Clears `DM-EXTRA-001` (`should`, jsonrpc only), taking jsonrpc to zero
    failures at every level.
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    83f1cf8 View commit details
    Browse the repository at this point in the history
  7. feat(rest): serve HTTP+JSON responses as application/a2a+json (#1274)

    ## What
    
    Spec
    [11.1](https://a2a-protocol.org/latest/specification/#111-protocol-requirements):
    "**Content-Type:** application/a2a+json **SHOULD** be used for requests
    and responses". The spec uses it in every worked HTTP+JSON example -
    20-plus of them.
    
    Every REST response inherited Starlette's `JSONResponse.media_type`
    default of `application/json`. `application/a2a+json` appeared nowhere
    in the repo, so the constant is new.
    
    ## Worth knowing: nobody implements this
    
    All four SDKs with published ACTS metrics - python, go, java, rust -
    fail `REST-CT-001`. It is a SHOULD, so it blocks no one's conformance,
    and a reasonable response to this PR is "not yet, let the others move
    first". Merging it makes a2a-python the first. Flagging it rather than
    burying it, since the other six PRs in this stack all had at least one
    SDK already passing to corroborate them.
    
    ## Two limits, both deliberate
    
    **Success responses only.** The error path runs through
    `rest_error_handler`, which `compat/v0_3/rest_adapter.py` shares. 11.1
    is the 1.0 binding's requirement, and v0.3 predates the media type, so
    changing that decorator would alter a different protocol version's wire
    format. Splitting it would mean making a bare decorator parameterized
    and touching every call site in both bindings - not worth it for a
    SHOULD the corpus does not test on the error path. The result is that
    1.0 errors still answer `application/json`; worth a follow-up, not worth
    bundling here.
    
    **Responses only, not requests.** 11.1 says both, but changing what the
    client *sends* risks servers that only accept `application/json`, and no
    test covers it.
    
    **No `JSONResponse` subclass.** It would have reduced nine sites to an
    import swap, but `rest_dispatcher.py` aliases `JSONResponse = Any` when
    Starlette is absent, and `class X(Any)` is a `TypeError` on Python 3.10,
    which this package supports. Passing `media_type=` explicitly keeps the
    no-Starlette path importable and the sites greppable.
    
    ## Result
    
    Clears `REST-CT-001` (`should`, rest only). With it, **all three
    bindings are at zero failures at every level**:
    
    | binding | passed | must | failed |
    | --- | --- | --- | --- |
    | jsonrpc | 101/111 | 59/59 | 0 |
    | grpc | 88/111 | 47/47 | 0 |
    | rest | 92/111 | 49/49 | 0 |
    
    The remainder are skips whose preconditions the fixture does not meet,
    not failures.
    JakubWorek authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    dc5a5da View commit details
    Browse the repository at this point in the history
  8. feat(server): add multi-replica cluster mode (#1281)

    Adds a2a.server.cluster, key interfaces (__VersionedTaskStore__ and
    __TaskEventStream__) for running the A2A server as several replicas,
    with no need to route a task's requests back to the replica that started
    it, and wires DefaultRequestHandlerV2 to use them so any replica can
    serve any request for a remote task. __Opt-in and backward compatible:__
    with no event stream and a plain (non-versioned) TaskStore, pre-existing
    behavior is preserved.
    
    __Key changes:__
    - VersionedTaskStore - a task store that keeps track of which revision
    of a task a replica last read. When that replica saves, the store checks
    the task has not changed in the meantime and refuses the write / retries
    if it has, instead of quietly overwriting whatever another replica just
    did. VersionedDatabaseTaskStore is the database-backed implementation.
    - TaskEventStream - a shared record of the events a task produces, so a
    replica that is not running the agent can still see the event stream.
    This lets a client reconnect to any replica and keep receiving updates
    for a task running elsewhere. DatabaseTaskEventStream is the
    database-backed implementation.
    - Schema for both: a new task_versions table and a new task_events
    table. The tasks table is unchanged.
    - New optional event_stream argument; task_store now accepts a plain or
    a versioned store and is still exposed unchanged as self.task_store.
    - Cancel works from any replica. The cancelling replica records the
    cancellation in the shared store, and the replica actually running the
    agent notices on its next save and stops.
    - Resubscribe works from any replica. It returns the current task, then
    follows the shared event stream from that point on, so the client sees
    no gap and no duplicates.
    - Alembic revision b5e3d1c8a2f7 (revises 38ce57e08137) creates the
    task_versions table and the task_events table with an index on task_id.
    It only creates tables and does not alter existing ones.
    - A runnable sample showing how to deploy the server as several
    replicas: it starts N replicas as separate processes sharing one
    database, and a driver starts a task on one replica, watches it from a
    second, and cancels it from a third. Defaults to a shared SQLite file so
    it runs with no external services; set A2A_CLUSTER_DSN to point every
    replica at Postgres or MySQL instead.
    
    __Tested:__
    - Integration tests run two DefaultRequestHandlerV2 replicas against one
    real Postgres or MySQL database, each replica on its own engine. They
    cover send, resubscribe and cancel handled by a replica other than the
    one running the agent, and a multi-turn input_required conversation that
    moves between replicas.
    - An integration test sends follow-ups to the same task from both
    replicas at once and asserts that neither turn is lost.
    - Unit tests cover the same cross-replica flows without a database,
    using in-memory versions of the shared store and event stream.
    - Unit tests for the migration's full up/down cycle, idempotency and
    offline SQL generation.
    - Adds the database-backed cluster tests to scripts/run_db_tests.sh.
    
    Run the sample from the repository root:
    ```
    python -m samples.clustermode.run_cluster --replicas 3 --base-port 41241
    python -m samples.clustermode.exercise --host 127.0.0.1 --ports 41241,41242,41243
    ```
    
    __Design discussion:__
    #1224
    
    Release-As: 1.2.0
    mykytanetipa authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    942d621 View commit details
    Browse the repository at this point in the history
  9. chore(main): release 1.2.0 (#1266)

    🤖 I have created a release *beep* *boop*
    ---
    
    
    ##
    [1.2.0](v1.1.5...v1.2.0)
    (2026-09-29)
    
    
    ### Features
    
    * **rest:** serve HTTP+JSON responses as application/a2a+json
    ([#1274](#1274))
    ([dc5a5da](dc5a5da))
    * **server:** add caching headers to the agent card endpoint
    ([#1272](#1272))
    ([83d7f5d](83d7f5d))
    * **server:** add multi-replica cluster mode
    ([#1281](#1281))
    ([942d621](942d621))
    * **server:** add opt-in validation of message media types against the
    agent card
    ([#1269](#1269))
    ([8037b24](8037b24))
    
    
    ### Bug Fixes
    
    * compare in-memory task timestamps numerically
    ([#1233](#1233))
    ([0d5473c](0d5473c))
    * **server:** ignore unrecognized request fields and fix parse-error
    data shape
    ([#1273](#1273))
    ([83f1cf8](83f1cf8))
    * **server:** reject a message whose contextId disagrees with its task
    ([#1270](#1270))
    ([c25022f](c25022f))
    * **server:** reject terminal-task operations with
    UnsupportedOperationError
    ([#1268](#1268))
    ([6cce91b](6cce91b))
    * **server:** send PushNotificationConfig.authentication as an
    Authorization header
    ([#1271](#1271))
    ([5751d31](5751d31)),
    closes [#585](#585)
    
    
    ### Documentation
    
    * **samples:** add Agent Card signing sample
    ([#1198](#1198))
    ([f3ac824](f3ac824))
    
    ---
    This PR was generated with [Release
    Please](https://github.com/googleapis/release-please). See
    [documentation](https://github.com/googleapis/release-please#release-please).
    a2a-bot authored Sep 29, 2026
    Configuration menu
    Copy the full SHA
    5ca9859 View commit details
    Browse the repository at this point in the history
Loading