Skip to content

Conversation

@tchapacan
Copy link

@tchapacan tchapacan commented Oct 26, 2025

Description

Add HTTP+REST ServerIde Transport Support

  • Server: REST endpoints/routes addition by taking advantage of pre-existing express from the codebase. Following this doc and internally rerouting to each a2a handler method containing the buisness logic

Some part of the code has been generated using generative AI to speed up the development, I have reviewed it carefully, but this should be taken into account into the review. Keep in mind this is my first time contributing to the repo and this is an attempt to help a2a-js support REST (HTTP+JSON) as per describe in the a2a documentation. I'll be happy to refacto it during review process, don't hesitate to challenge it.


Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Follow the CONTRIBUTING Guide.
  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
    • Important Prefixes for release-please:
      • fix: which represents bug fixes, and correlates to a SemVer patch.
      • feat: represents a new feature, and correlates to a SemVer minor.
      • feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
  • Ensure the tests and linter pass
  • Appropriate docs were updated (if necessary)

Fixes #137 🦕

@tchapacan tchapacan force-pushed the feat/implement-http+json branch from ab133cc to 530d5db Compare October 27, 2025 00:07
@ishymko
Copy link
Member

ishymko commented Oct 30, 2025

Thanks a lot @tchapacan for looking into this! Appreciate your effort to align this proposal with the existing infrastructure.

I actually think that current way of wiring it up with express followed in this repo worth changing to make it more modular and explicit, i.e.

const app = express();

// const a2aRequestHandler = ...
// const agentCard = ...

app.use('/.well-known/agent-card.json', agentCardHandler(agentCard));
app.use('/a2a/jsonrpc', jsonRpcHandler(a2aRequestHandler));
app.use('/a2a/api', restHandler(a2aRequestHandler));

This is just a sketch, but the idea is to provide ready to use routers which can be registered independently by the integration code, currently A2AExpressApp uses provided app only to register internal router under a provided basePath, effectively wrapping basic express API. I'll try to draft a PR with this so that REST can follow a new API.

In addition, current JSON-RPC implementation uses framework-agnostic JsonRpcTransportHandler (see also Python SDK) which is not the case for REST in this proposal. Maybe there is not much which can be made framework-agnostic for REST though, but it worth checking if we can make both transports consistent implementation-wise.

Similar applies to client, JSON-RPC specific implementation has to be factored out into some sort of a "transport" abstraction which can be implemented for REST as well (see Python). I'll try to prepare this as well.

And last but not least, we're setting up conformance tests from here: #159. Once SUT agent is ready, it'll be possible to validate REST transport as well.

@tchapacan
Copy link
Author

tchapacan commented Nov 1, 2025

Hello @ishymko ! No problem hope it helped a little, at least starting the job for REST support, and opening the discussion!

Yep, I like it too! It seems more modular and easier to understand/maintain in the end. And going with some modular middleware can be compatible with other framework such as fastify see this https://github.com/fastify/fastify-express.

I was thinking about going with the framework agnostic too, following the jsonRcpTransportHandler. But in the end I choose to leverage fully of express framework which does everything we need (routing, method, etc..), only caveats was the route like v1/message:stream where the regex is kind of necessary as it seems express don’t support this kind of route. In the end it only expose business logic from a2a handling method, into express middleware, I don’t see much to do rather than re-implementing what express does really great. I let you decide what would be best, I understand having consistency in the codebase is a plus too.

Agree for the client too, I’m not really happy with my implementation even if it was working, but it deserves some love to make it cleaner.

Good for the conformance test in the CI! Once this will be implemented, you can take advantage of this draft PR to test it.

If you need any help don’t hesitate, on the client part or server part, depending of my time I’ll be happy to help!

swapydapy pushed a commit that referenced this pull request Nov 13, 2025
# Description

As a preparation step for REST support proposed in #142, extract
Express.js handlers from `A2AExpressApp` to allow assembling your own
app using Express.js API directly for specifying individual paths and
middlewares, rather than wrapping it in `A2AExpressApp`:

```ts
// const a2aRequestHandler = . . .

const app = express();

// Minimal example:
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: a2aRequestHandler }));
app.use(jsonRpcHandler({ requestHandler: a2aRequestHandler }));

// Advanced example:
app.use(myCustomMiddleware());
app.use('/.well-known/custom-agent-card.json', myCustomAgentCardMiddleware, agentCardHandler({ agentCardProvider: a2aRequestHandler }));
app.use('/a2a/jsonrpc', myJsonRpcMiddleware, jsonRpcHandler({ requestHandler: a2aRequestHandler }));
app.use('/a2a/api', restHandler({ requestHandler: a2aRequestHandler }));
```

No logic changes to existing handlers, they are moved as is. Also
`A2AExpressApp` is switched to use new handlers without behavior
changes.

**Note:** new functions **are not** exported via `index.ts` yet, it will
be done together with updating docs and examples once REST support is
done.

Re #137
@ishymko
Copy link
Member

ishymko commented Nov 13, 2025

Hi @tchapacan, thank you for you patience!

At this point server-side implementation should be good to proceed as #162 is merged and we also have TCK tests running.

Could you please merge with main and factor out the server side support in the same fashion as in #162? As you proposed in the description, splitting server and client here indeed will help a lot, so let's start with the server as client is not yet switched to multi-transport types.

Also please make sure to integrate with TCK tests. It should probably work with minimal changes to CLI arguments when SUT agent is updated with REST transport in agent card.

@tchapacan tchapacan force-pushed the feat/implement-http+json branch 4 times, most recently from 71285cd to 2e758e7 Compare November 14, 2025 23:59
@tchapacan
Copy link
Author

tchapacan commented Nov 15, 2025

Hello @ishymko! Thanks for following it up with #162 and the TCK testing, it seems clear!

I've rebased on main and tried to implement the server-side REST support following the same pattern as #162. Here's what I have done:

Server Side REST support:

  • Created httpRestHandler() in src/server/express/http_rest_handler.ts following the same pattern as jsonRpcHandler() and agentCardHandler()
  • Handlers are exported from src/server/express/index.ts and A2AExpressApp remains for backward compatibility
  • REST routes follow the pattern: /v1/message:send, /v1/message:stream, /v1/task/:taskId, etc.

TCK adaptation:

  • Updated tck/agent/index.ts to mount both transports using new middleware:
    • JSON-RPC at /a2a/jsonrpc
    • REST at /a2a/rest
  • Updated .github/workflows/run-tck.yaml to test both transports (cli argument and url changed)
  • Agent card now includes both transport URLs

Not sure if the implementation is correct for the TCK agent and the workflow (cli argument), I have tried to follow the docs from this repo but LMK if what I have done make sense to be sure testing is relevant. A2aExpress is not tested anymore here (deprecated) as I'm leveraging the new middleware and not sure if the combination between the agentCard (JSONRPC + REST) and the cli argument passed in the workflow are really OK.

Something I would like to highlight while I was testing my implementation with the TCK test :

I encountered an issue: the TCK cli for REST requests use snake_case (e.g., message_id, context_id), but internal TypeScript types use camelCase (e.g., messageId, contextId). This seems pretty standard and normal - REST APIs typically are snake_case but TypeScript uses camelCase here. That’s why the TCK test was failing during my first attempt. To overcome this I went this path and tried to :

Implement a transformation middleware in (src/server/express/utils.ts) used internally in httpRestHandler that will:

  • Transforms incoming keys from bodies REST requests: snake_case → camelCase
  • Transforms outgoing key from bodies of responses: camelCase → snake_case

Implementation is quite loose and based on regex, so not sure it's 100% safe to go this path, but at least it solve the TCK tests failing (locally, but partially in the CI)

Even with this modification, I have some inconsistency between my local run of a2a-tck which are passing :

./run_tck.py --sut-url http://localhost:41241/a2a/rest --category mandatory --transports rest --transport-strategy prefer_rest --verbose
======================================================================
🚀 Running MANDATORY tests
Description: Mandatory A2A compliance tests
======================================================================

Command: a2a-tck/.venv/bin/python3 -m pytest tests/mandatory/protocol/ --sut-url=http://localhost:41241/a2a/rest --test-scope=all --tb=short -m mandatory_protocol -v --transport-strategy prefer_rest --transports rest --enable-equivalence-testing

================================================================================================ test session starts =================================================================================================
platform darwin -- Python 3.14.0, pytest-9.0.1, pluggy-1.6.0 -- a2a-tck/.venv/bin/python3
cachedir: .pytest_cache
metadata: {'Python': '3.14.0', 'Platform': 'macOS-15.6-arm64-arm-64bit-Mach-O', 'Packages': {'pytest': '9.0.1', 'pluggy': '1.6.0'}, 'Plugins': {'anyio': '4.11.0', 'json-report': '1.5.0', 'metadata': '3.1.1', 'asyncio': '1.3.0'}}
rootdir: a2a-tck
configfile: pyproject.toml
plugins: anyio-4.11.0, json-report-1.5.0, metadata-3.1.1, asyncio-1.3.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 35 items / 14 deselected / 21 selected

tests/mandatory/protocol/test_a2a_v030_new_methods.py::TestAuthenticatedExtendedCard::test_authenticated_extended_card_method_exists SKIPPED (SUT does not support authenticated extended card)                [  4%]
tests/mandatory/protocol/test_a2a_v030_new_methods.py::TestAuthenticatedExtendedCard::test_authenticated_extended_card_without_auth SKIPPED (SUT does not support authenticated extended card)                 [  9%]
tests/mandatory/protocol/test_a2a_v030_new_methods.py::TestMethodMappingCompliance::test_core_method_mapping_compliance PASSED                                                                                 [ 14%]
tests/mandatory/protocol/test_a2a_v030_new_methods.py::TestMethodMappingCompliance::test_transport_specific_method_naming PASSED                                                                               [ 19%]
tests/mandatory/protocol/test_a2a_v030_transport_compliance.py::test_transport_compliance_validation PASSED                                                                                                    [ 23%]
tests/mandatory/protocol/test_a2a_v030_transport_compliance.py::test_required_method_availability PASSED                                                                                                       [ 28%]
tests/mandatory/protocol/test_a2a_v030_transport_compliance.py::test_transport_specific_features PASSED                                                                                                        [ 33%]
tests/mandatory/protocol/test_a2a_v030_transport_compliance.py::test_multi_transport_method_mapping SKIPPED (Multi-transport method mapping requires 2+ transports)                                            [ 38%]
tests/mandatory/protocol/test_a2a_v030_transport_compliance.py::test_comprehensive_a2a_v030_compliance PASSED                                                                                                  [ 42%]
tests/mandatory/protocol/test_agent_card.py::test_agent_card_available PASSED                                                                                                                                  [ 47%]
tests/mandatory/protocol/test_agent_card.py::test_mandatory_fields_present PASSED                                                                                                                              [ 52%]
tests/mandatory/protocol/test_agent_card.py::test_mandatory_field_types PASSED                                                                                                                                 [ 57%]
tests/mandatory/protocol/test_message_send_method.py::test_message_send_valid_text PASSED                                                                                                                      [ 61%]
tests/mandatory/protocol/test_message_send_method.py::test_message_send_invalid_params PASSED                                                                                                                  [ 66%]
tests/mandatory/protocol/test_message_send_method.py::test_message_send_continue_task PASSED                                                                                                                   [ 71%]
tests/mandatory/protocol/test_state_transitions.py::test_task_history_length PASSED                                                                                                                            [ 76%]
tests/mandatory/protocol/test_tasks_cancel_method.py::test_tasks_cancel_valid PASSED                                                                                                                           [ 80%]
tests/mandatory/protocol/test_tasks_cancel_method.py::test_tasks_cancel_nonexistent PASSED                                                                                                                     [ 85%]
tests/mandatory/protocol/test_tasks_get_method.py::test_tasks_get_valid PASSED                                                                                                                                 [ 90%]
tests/mandatory/protocol/test_tasks_get_method.py::test_tasks_get_with_history_length PASSED                                                                                                                   [ 95%]
tests/mandatory/protocol/test_tasks_get_method.py::test_tasks_get_nonexistent PASSED                                                                                                                           [100%]

=================================================================================== 18 passed, 3 skipped, 14 deselected in 34.81s ====================================================================================

And the one in the CI that are failing example => https://github.com/a2aproject/a2a-js/actions/runs/19397175092/job/55498766100?pr=142

Might need your help about this to understand what I'm doing wrong ?

Performance Impact:
I've tried to do a simple load testing with autocannon using the v1/message:send route (to make sure it wasn't killing performance, even If the inference time would always be way greater) :

  • Test config (tried to be the closest to a realistic scenario): 20 concurrent connections, 30s duration, pipelining set to 1 (sequential message)
  • result show ~5-10% performance overhead from the transformation middleware (check below)
  • Note: I had to comment this line in the TCK agent's for the processing fake delay to have relevant metrics during the load test

With middleware :

Running 30s test @ http://localhost:41241/a2a/rest/v1/message:send
20 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max    │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼────────┤
│ Latency │ 1 ms │ 2 ms │ 8 ms  │ 9 ms │ 2.45 ms │ 3.97 ms │ 135 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬──────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg      │ Stdev    │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼──────────┼─────────┤
│ Req/Sec   │ 3 017   │ 3 017   │ 7 139   │ 7 775   │ 6 715,44 │ 1 073,88 │ 3 016   │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼──────────┼─────────┤
│ Bytes/Sec │ 4.13 MB │ 4.13 MB │ 9.78 MB │ 10.6 MB │ 9.19 MB  │ 1.47 MB  │ 4.13 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴──────────┴─────────┘
┌──────┬────────┐
│ Code │ Count  │
├──────┼────────┤
│ 201  │ 201453 │
└──────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 30

201k requests in 30.02s, 276 MB read

Without middleware :

Running 30s test @ http://localhost:41241/a2a/rest/v1/message:send
20 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max    │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼────────┤
│ Latency │ 1 ms │ 1 ms │ 8 ms  │ 9 ms │ 2.13 ms │ 4.15 ms │ 169 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬───────┬──────────┬──────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5% │ Avg      │ Stdev    │ Min     │
├───────────┼─────────┼─────────┼─────────┼───────┼──────────┼──────────┼─────────┤
│ Req/Sec   │ 1 887   │ 1 887   │ 7 843   │ 8 123 │ 7 284,54 │ 1 407,15 │ 1 887   │
├───────────┼─────────┼─────────┼─────────┼───────┼──────────┼──────────┼─────────┤
│ Bytes/Sec │ 2.56 MB │ 2.56 MB │ 10.6 MB │ 11 MB │ 9.88 MB  │ 1.91 MB  │ 2.56 MB │
└───────────┴─────────┴─────────┴─────────┴───────┴──────────┴──────────┴─────────┘
┌──────┬────────┐
│ Code │ Count  │
├──────┼────────┤
│ 201  │ 218506 │
└──────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 30

219k requests in 30.02s, 297 MB read

I'm open to different approaches here and would like your guidance for the next steps:

  1. Should the a2a-tck compliance tests be changed to use camelCase for REST endpoints instead of snake_case?
  2. Should we introduce REST-specific types (e.g., RestMessageSendParams) with snake_case naming?
  3. Is the transformation middleware acceptable given the « apparently » ~5-10% overhead, or you prefer a different approach ?

Let me know what you think would be best! Happy to adjust the implementation, reorganize the code (convention naming etc..) and tests following your recommendations. But first I need a little help related to the a2a-tck issue I'm having.

@tchapacan tchapacan force-pushed the feat/implement-http+json branch 5 times, most recently from 0746260 to 3343f3a Compare November 17, 2025 09:03
@ishymko
Copy link
Member

ishymko commented Nov 17, 2025

Thank you @tchapacan! This is a very solid work! I'll get to review it properly once we have the testing sorted out, but I agree with the general approach.

On TCK
Can we do a single step via

./run_tck.py --sut-url URL --category mandatory \
  --transports jsonrpc,rest

(modified example Run per-transport single-client tests for JSON-RPC and gRPC, then equivalence from here)

This should also make use of additional interfaces you defined in the agent card, as transport specific base URLs should be discovered automatically from it without defining them explicitly as SUT URL.

As for the differences between CI run and local run: are you sure you're running against the same version of TCK locally? CI uses 0.3.0.beta3 tag from the a2a-tck repo. Also it can be due to some state leftovers from the JSON-RPC run... So worth running both locally to see if you can reproduce the CI failure. Please let me know if it doesn't help and I'll have another look.

On snake_case/camelCase
Yeah good catch, seems like other languages use underscores. It's actually not specified very well in the protocol spec itself. My vote is option 2: REST-specific types. It's a bit of boilerplate but draws a clear boundary between transport specific and core types. I agree with you that automatic conversion is not fully safe, also we may have more differences in the future.

@ishymko
Copy link
Member

ishymko commented Nov 19, 2025

Hi @tchapacan, as a heads up we've recently merged ESLint and Prettier integration (#183, #185) which caused substantial diffs across the whole codebase. Those are only formatting and "unused imports" kind of changes, so you should be good with just resolving all the conflicts in favor of your local state and applying npm run lint:fix on top of that.

ishymko added a commit that referenced this pull request Nov 19, 2025
# Description

Currently `A2AClient` defined in `client.ts` uses JSON-RPC types in its
interface, i.e. `sendMessage` uses
[`SendMessageResponse`](https://github.com/a2aproject/a2a-js/blob/e7e8f35d5d5356d30a13131dea0caff97be8cd53/src/client/client.ts#L207)
which contains [JSON-RPC fields like `id` and `jsonrpc`
version](https://github.com/a2aproject/a2a-js/blob/e7e8f35d5d5356d30a13131dea0caff97be8cd53/src/types.ts#L2177-L2190).
It was also the case for Python SDK which required introducing a new
[client.py](https://github.com/a2aproject/a2a-python/blob/main/src/a2a/client/client.py)
keeping the old one as
[legacy.py](https://github.com/a2aproject/a2a-python/blob/main/src/a2a/client/legacy.py)
for backward compatibility (see
[#348](a2aproject/a2a-python#348)).

As a first step of introducing a transport-agnostic client, extract
JSON-RPC specific logic into a transport abstraction and switch existing
`A2AClient` to it in a backward compatible way to take advantage of
existing tests.

**Note:** new types are not exported from `index.ts`, this will be done
once new client is ready.

# Changes

1. Define `A2ATransport` abstraction without using JSON-RPC types.
2. Implement `JsonRpcTransport` from existing `A2AClient`.
3. Use new `JsonRpcTransport` from `A2AClient` - new transports are
**not going** to be added into this client as it uses JSON-RPC types.
This is done in a backward compatible way, existing behavior is
preserved: returning JSON-RPC errors as objects, populating JSON-RPC
specific fields.
4. Define shared transport-agnostic errors for A2A specific errors
defined in [the
spec](https://a2a-protocol.org/latest/specification/#82-a2a-specific-errors).

Re #137, #142
@tchapacan
Copy link
Author

Hello @ishymko

Thanks for the heads-up! Will do for the rebase and the linting no problem about that!

I'm able to reproduce the behavior of the tck CI run by using the correct tag 0.3.0.beta3 locally! I missed the details to switch to the correct tag my bad sorry!

I was able to dig a little further and it seems the object message send by the tck cli is not same as the one expected by the server. Using the 0.3.0.beta3 tag it fails, but it seems to have been fixed in main branch, so maybe there is just a new release to perform?

Following the cli arguments you recommended and by performing some adjustment into the agentcard by following more rigorously this part of the spec, here what I got now :

./run_tck.py --sut-url URL --category mandatory --transports jsonrpc,rest

Using tag 0.3.0.beta3 (failing):

  • JSONRPC:
{
  kind: 'message',
  messageId: 'text-message-id-54f8f97e-994f-4270-8d35-55623ecf6c5f',
  role: 'user',
  parts: [ { kind: 'text', text: 'Hello from TCK!' } ],
  contextId: '16f7759f-7f57-469a-8e1c-54a169300150'
}
  • REST:
{
  messageId: 'text-message-id-394c0fa2-7548-4c9f-ab01-776bc8ec0426',
  role: 'ROLE_USER',
  content: [ { text: 'Hello from TCK!' } ],
  contextId: '8d0ac3ed-1fcf-45f1-920b-671a5a0f936f'
}

Using main branch (all good):

  • JSONRPC:
{
  kind: 'message',
  messageId: 'text-message-id-851f3e53-ac53-42ab-877d-5ac943b986e3',
  role: 'user',
  parts: [ { kind: 'text', text: 'Hello from TCK!' } ],
  contextId: '054bbefe-7e20-4abb-9974-de00f29f795d'
}
  • REST:
{
  messageId: 'text-message-id-458fd959-e5d9-4079-a88e-4ba1ca1a6aa3',
  role: 'ROLE_USER',
  parts: [ { text: 'Hello from TCK!' } ],
  contextId: '1a7060d9-23dc-4f4f-99e5-f5554dc66f31'
}

I will remove the transformation middleware in my next commits/push and try my best to propose some relevant type for REST, I'll work on it in the next days.

@tchapacan tchapacan force-pushed the feat/implement-http+json branch 6 times, most recently from 0a8cc83 to fde96cd Compare November 20, 2025 11:50
@ishymko
Copy link
Member

ishymko commented Nov 20, 2025

Hello @tchapacan!

Great catch on main vs 0.3.0.beta3! Seems like indeed it was using incorrect payload (content vs parts as in your example) which fails in our SUT agent

const textPart = message.parts.find((part) => part.kind === 'text');
at undefined.find and it got fixed in main. I don't see any new releases created in TCK, setting main in the workflow is fragile, as it'll be floating, can we set it to the fix commit directly for now (actions/checkout should support it)?

@tchapacan tchapacan force-pushed the feat/implement-http+json branch 2 times, most recently from e5aaa8b to 8821ab8 Compare November 21, 2025 19:41
@tchapacan tchapacan force-pushed the feat/implement-http+json branch from 8821ab8 to b66a396 Compare November 21, 2025 19:49
@tchapacan
Copy link
Author

Hello @ishymko!

I have sticked to this commit for the tck ref in the workflow while waiting for a new tag there, no problem it works very well!

I have removed the "magic" middleware, and tried to implement some custom snake-case types for REST (added in types.ts) that will be used at the http level in the handler, internally the usual a2a camelCase types are kept. There is still a need of transformation by doing this way, so I have added a couple of functions for that. Difference versus before is that they are explicit now, and not magically modifying any snake-case into camelCase. Let me know if it match the spirit you expected.

For an express api generally I like to define my schema using zod library, infer my types from the schema (feature from zod) and perform the request validation using the schema. Some transformation can be done as well. But I didn't want to introduce an external library, and doing it natively for the complexity we have seems acceptable.

If that's ok for you, I was thinking about extracting some part of the logic into the transport folder (like you were proposing before), and only keep the route declaration in the handler if relevant, to better organize the code, but honestly I let you decide what's best for this!

LMK if it's ok to switch this PR to open for review as we are now or if it still need some work before. I suppose documentation will have to be updated as well, but don't know if we do it in this PR or in an another one, I can of course help!

@ishymko
Copy link
Member

ishymko commented Nov 24, 2025

Hello @tchapacan! Thank you very much for all your work and being responsive!

some custom snake-case types for REST (added in types.ts)

Unfortunately we can't modify types.ts at the moment, as this file is generated (see here). Can we collocate types with HTTP+JSON implementation instead? I think those types can stay private as they should only serve its purpose for this transport.

Difference versus before is that they are explicit now, and not magically modifying any snake-case into camelCase. Let me know if it match the spirit you expected.

Yes, exactly!

But I didn't want to introduce an external library, and doing it natively for the complexity we have seems acceptable.

Sounds reasonable to me! We can always change it later if manual validation becomes too complex.

If that's ok for you, I was thinking about extracting some part of the logic into the transport folder (like you were proposing before), and only keep the route declaration in the handler if relevant

Sounds good to me, let's try to do it.

LMK if it's ok to switch this PR to open for review as we are now or if it still need some work before. I suppose documentation will have to be updated as well, but don't know if we do it in this PR or in an another one

Feel free to do it once you're ready with the items above and assign me as a reviewer. On the documentation, yes, for now let's skip it, I was planning to do it closer to the next version release to keep the main readme consistent. One can always switch tags to see relevant documentation, but given significant changes, don't want to confuse people with the main page readme opened by default which reflects unreleased version (and I don't want to maintain a dedicated branch for this as well).

@tchapacan tchapacan force-pushed the feat/implement-http+json branch from d7de73d to c1455ab Compare November 25, 2025 22:13
@tchapacan
Copy link
Author

tchapacan commented Nov 25, 2025

Hello @ishymko! You are welcome, no problem always happy to stay up to date and contribute/help when possible!

To summarize latest change :

  • created a rest_type.ts file and moved the REST types in here, LMK if it's OK
  • extracted some part of the http_rest_handler.ts into transports/http_rest_transport_handler.ts

I tried to keep in the express handler only the pure express part, and all the remaining transformation, SSE formatting, etc in the transport part

  • rebase upstream from main branch

Make sense for the docs, It can be done later (just before a future release). I'm opening the PR for review but it seems I can't add you as reviewer, so poking you here. Don't hesitate to challenge it, specifically convention naming or code organization, I'm open for any changes.

Let's not forget the version of tck as well that is sticked into the github workflow currently to this commit, if it goes in I can open a small issue for that to avoid It's forgotten later.

@tchapacan tchapacan force-pushed the feat/implement-http+json branch from c1455ab to 56ee1a0 Compare November 25, 2025 22:31
@tchapacan tchapacan changed the title feat: implement http+json feat: implement server http+json Nov 25, 2025
@tchapacan tchapacan marked this pull request as ready for review November 25, 2025 22:34
@ishymko ishymko self-requested a review November 26, 2025 10:00
Copy link
Member

@ishymko ishymko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @tchapacan, thanks for wiring up everything together, this is a solid work!

First pass done, added a few comments on potential reshuffling things a bit, also let's double check SSE/streaming implementation as for some reason it fails TCK test (the one we don't run now, you'll find more details in the specific comment).

Let's not forget the version of tck as well that is sticked into the github workflow currently to this commit, if it goes in I can open a small issue for that to avoid It's forgotten later.

Sounds good, let's create a follow-up once this one is merged, thank you!

# TODO once we have the TCK for 0.4.0 we will need to look at the branch to decide which TCK version to run.
# Tag of the TCK
TCK_VERSION: 0.3.0.beta3
TCK_VERSION: 601bf0d6f63baafb079c2ab23f53d73ce03e5461
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment here why it was done, maybe referencing this PR comment(s) with our discussion?

})
);
router.use(`/${agentCardPath}`, agentCardHandler({ agentCardProvider: this.requestHandler }));
router.use(httpRestHandler({ requestHandler: this.requestHandler }));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove it from the A2AExpressApp to avoid registering unexpected new routes by default. We're going to move to the new handlers to be explicit and deprecate the A2AExpressApp.

if (err instanceof SyntaxError && 'body' in err) {
const a2aError = A2AError.parseError('Invalid JSON payload.');
return res.status(400).json({
error: a2aError.toJSONRPCError(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be using JSON-RPC errors here, current v0.3 spec doesn't specify the payload of HTTP+JSON errors, but using JSON-RPC still doesn't feel right to me. Let's follow Python implementation for now.

The v1.0 is going to specify it, see in the draft here and here. We will support it when getting ready for 1.0 (and when the final spec version is published).

Applies to other places using toJSONRPCError().

Comment on lines +472 to +477
/**
* @deprecated Use httpRestHandler instead.
*/
export function createHttpRestRouter(requestHandler: A2ARequestHandler): Router {
return httpRestHandler({ requestHandler }) as Router;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a leftover, can we remove it?

Comment on lines +73 to +76
const ROUTE_PATTERN = {
MESSAGE_ACTION: /^\/v1\/message:(send|stream)$/i,
TASK_ACTION: /^\/v1\/tasks\/([^/:]+):([a-z]+)$/i,
} as const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's consider simplifying it so that single router.get/route.post handles one method to make the logic more explicit and avoid advanced regex matching. Code duplication between similar "groups" of actions can be extracted into helper methods.

export type { JsonRpcHandlerOptions } from './json_rpc_handler.js';
export { agentCardHandler } from './agent_card_handler.js';
export type { AgentCardHandlerOptions, AgentCardProvider } from './agent_card_handler.js';
export { httpRestHandler, restErrorHandler } from './http_rest_handler.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restErrorHandler should stay internal, it's a part of httpRestHandler.

Suggested change
export { httpRestHandler, restErrorHandler } from './http_rest_handler.js';
export { httpRestHandler } from './http_rest_handler.js';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually introduce a class here similar to JSON-RPC which will perform the type conversion and some validation (like for history length and also capabilities check)? This way it should be possible to shrink down the express part more, the purpose of express part will be to create a transport specific object (i.e. assemble data from body and query params) and pass it down here, where it's going to be converted into the "domain" object, validated and processed.

}

// ============================================================================
// Server-Sent Events (SSE) Support
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to reuse SSE code from jsonrpc_transport_handler.ts? There is an issue for this #175. If it gets too troublesome we can tackle it later, either way as long as it's not exported we can reorganize any time.

}

// ============================================================================
// Server-Sent Events (SSE) Support
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to run TCK capabilities tests and got SSE/streaming for REST specifically (it passes for JSON-RPC). Currently we don't run capabilities in CI, only mandatory.

Failures:

FAILED tests/optional/capabilities/test_streaming_methods.py::test_message_stream_basic - AssertionError: assert 'taskId' in {'context_id': '065ae425-f162-4ab0-b618-18e7c29bcce3', 'final': False, 'kind': 'status-update', 'status': {'message': ... 'text', 'text': 'Processing your question'}], ...}, 'state': 'working', 'timestamp': '2025-11-26T10:45:11.634Z'}, ...}
FAILED tests/optional/capabilities/test_streaming_methods.py::test_sse_event_format_compliance - tck.transport.base_client.TransportError: [REST] Unexpected error in REST streaming: Event loop is closed

I run it against your branch (TCK against the commit set for CI in this PR) by:

  1. cd tck && npm run tck:sut-agent
  2. uv run ./run_tck.py --sut-url http://localhost:41241/a2a/jsonrpc --category capabilities --transports jsonrpc,rest

Could you please check it? If it's some HTTP+JSON implementation bug and you'll be able to get it working, let's include them in CI (there are only two failures in this category). If it's a bug in TCK we can take another look and maybe report it in their repo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be moved to /transports I think, i.e. under a new subcatalog transports/rest together with the other file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Support HTTP+JSON/REST

2 participants