fasta2a: bump to 0.6.0 with A2A spec compatibility fixes#1075
fasta2a: bump to 0.6.0 with A2A spec compatibility fixes#1075jensbodal wants to merge 1 commit intoagent0ai:developmentfrom
Conversation
jensbodal
commented
Feb 18, 2026
- Bump fasta2a to 0.6.0
- agent.json -> agent-card.json per A2A spec
- Use camelCase wire format (messageId, contextId)
- Set A2A_SERVER_URL to expose agent outside local network (defaults to localhost)
- Default acceptedOutputModes to ["application/json"] when omitted
- Return structured error to client on task failure
- Add A2A compatibility tests
- Bump fasta2a to 0.6.0 - agent.json -> agent-card.json per A2A spec - Use camelCase wire format (messageId, contextId) - Set A2A_SERVER_URL to expose agent outside local network (defaults to localhost) - Default acceptedOutputModes to ["application/json"] when omitted - Return structured error to client on task failure - Add A2A compatibility tests
There was a problem hiding this comment.
Pull request overview
This pull request updates the fasta2a integration to version 0.6.0 and implements several A2A (Agent-to-Agent) specification compliance improvements. The changes ensure compatibility with the official A2A protocol specification, including proper naming conventions, wire format standards, and error handling patterns.
Changes:
- Bumps fasta2a dependency from 0.5.0 to 0.6.0 and renames agent.json to agent-card.json per A2A spec
- Implements camelCase wire format for A2A protocol fields (messageId, contextId) to comply with the specification
- Adds A2A_SERVER_URL environment variable configuration, structured error messages on task failure, and acceptedOutputModes defaulting middleware
- Introduces comprehensive A2A compatibility tests using the official a2a-python client library
Reviewed changes
Copilot reviewed 5 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Updates fasta2a from version 0.5.0 to 0.6.0 |
| requirements2.txt | No functional change (formatting only) |
| requirements.dev.txt | No functional change (formatting only) |
| python/helpers/subagents.py | Renames agent.json to agent-card.json throughout, removes trailing whitespace |
| python/helpers/runtime.py | Adds get_a2a_server_url() function to construct A2A server URLs with token |
| python/helpers/fasta2a_server.py | Implements camelCase wire format, adds structured error handling, acceptedOutputModes defaulting, and A2A_SERVER_URL support |
| python/helpers/fasta2a_client.py | Updates to use agent-card.json paths and camelCase wire format with backward compatibility |
| tests/test_fasta2a_client.py | Replaces basic connectivity test with comprehensive A2A compatibility tests using official client library |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # A2A allows message/send.configuration.acceptedOutputModes to be optional. | ||
| # fasta2a 0.6.0 rejects configuration without it, so default to ["application/json"]. | ||
| original_receive = receive | ||
| request_buffered = False | ||
|
|
||
| async def receive_wrapper(): | ||
| nonlocal request_buffered | ||
| if request_buffered: | ||
| return await original_receive() | ||
|
|
||
| body_parts: list[bytes] = [] | ||
| while True: | ||
| message = await original_receive() | ||
| if message['type'] != 'http.request': | ||
| return message | ||
| body_parts.append(message.get('body', b'')) | ||
| if not message.get('more_body', False): | ||
| break | ||
|
|
||
| request_buffered = True | ||
| full_body = b''.join(body_parts) | ||
| if not full_body: | ||
| return {'type': 'http.request', 'body': b'', 'more_body': False} | ||
|
|
||
| try: | ||
| data = json.loads(full_body) | ||
| changed = False | ||
| if isinstance(data, dict) and data.get('method') == 'message/send': | ||
| params = data.get('params') | ||
| if isinstance(params, dict): | ||
| configuration = params.get('configuration') | ||
| if ( | ||
| isinstance(configuration, dict) | ||
| and not configuration.get('acceptedOutputModes') | ||
| ): | ||
| configuration['acceptedOutputModes'] = ['application/json'] | ||
| changed = True | ||
|
|
||
| if changed: | ||
| full_body = json.dumps(data).encode('utf-8') | ||
| except Exception as e: | ||
| _PRINTER.print(f"[A2A] Failed to normalize request payload: {e}") | ||
|
|
||
| return {'type': 'http.request', 'body': full_body, 'more_body': False} | ||
|
|
||
| receive = receive_wrapper |
There was a problem hiding this comment.
Two independent receive wrappers buffer the request body sequentially, creating inefficiency. The project injection wrapper (lines 498-543) buffers and modifies the body, then the acceptedOutputModes wrapper (lines 579-624) receives that already-buffered body and buffers it again before potentially modifying it further. While this works functionally, consider combining both modifications in a single wrapper to avoid double-buffering the request body and improve performance.
| a2a_server_url = ( | ||
| str(dotenv.get_dotenv_value("A2A_SERVER_URL", 'localhost')) | ||
| ) | ||
|
|
There was a problem hiding this comment.
The get_a2a_server_url function returns a URL that doesn't include the protocol scheme (http:// or https://). The function gets 'localhost' as the default value for A2A_SERVER_URL, but a valid URL should include the scheme. For example, if A2A_SERVER_URL is set to just 'example.com', the returned URL would be 'example.com/a2a/t-token', which is not a valid complete URL. Consider either requiring the scheme in the environment variable or adding logic to prepend it if missing, similar to what is done in the _get_rfc_url function at line 136.
| a2a_server_url = ( | |
| str(dotenv.get_dotenv_value("A2A_SERVER_URL", 'localhost')) | |
| ) | |
| a2a_server_url = str( | |
| dotenv.get_dotenv_value("A2A_SERVER_URL", "http://localhost") | |
| ) | |
| if "://" not in a2a_server_url: | |
| a2a_server_url = "http://" + a2a_server_url | |
| if a2a_server_url.endswith("/"): | |
| a2a_server_url = a2a_server_url[:-1] |
| a2a_server_url = ( | ||
| str(dotenv.get_dotenv_value("A2A_SERVER_URL", 'localhost')) | ||
| ) | ||
| a2a_url = f"{a2a_server_url}/a2a/t-{self.token}" |
There was a problem hiding this comment.
The URL construction logic is duplicated between runtime.py (lines 182-187) and fasta2a_server.py (lines 263-266). This duplication creates a maintenance issue where changes to the URL construction logic would need to be made in both places. Consider having fasta2a_server.py use the runtime.get_a2a_server_url function instead of duplicating the logic.