-
Notifications
You must be signed in to change notification settings - Fork 26
feat: extensions #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: extensions #44
Changes from all commits
3e60fa1
8a5dbfe
5903cff
d16abca
23907cf
dec5581
33ee9c5
1709673
2db81b8
ab97e62
e5e2be8
447153f
fe5d007
6c0f423
29cc9e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| .cache | ||
| __pycache__ | ||
| .coverage* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| from .applications import FastA2A | ||
| from .broker import Broker | ||
| from .schema import Skill | ||
| from .schema import AgentExtension, Skill, StreamEvent | ||
| from .storage import Storage | ||
| from .worker import Worker | ||
|
|
||
| __all__ = ['FastA2A', 'Skill', 'Storage', 'Broker', 'Worker'] | ||
| __all__ = [ | ||
| 'AgentExtension', | ||
| 'Broker', | ||
| 'FastA2A', | ||
| 'Skill', | ||
| 'Storage', | ||
| 'StreamEvent', | ||
| 'Worker', | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| A2AResponse, | ||
| AgentCapabilities, | ||
| AgentCard, | ||
| AgentExtension, | ||
| AgentInterface, | ||
| AgentProvider, | ||
| Skill, | ||
|
|
@@ -43,7 +44,9 @@ def __init__( | |
| description: str | None = None, | ||
| provider: AgentProvider | None = None, | ||
| skills: list[Skill] | None = None, | ||
| extensions: list[AgentExtension] | None = None, | ||
| docs_url: str | None = '/docs', | ||
| streaming: bool = True, | ||
| # Starlette | ||
| debug: bool = False, | ||
| routes: Sequence[Route] | None = None, | ||
|
|
@@ -68,7 +71,9 @@ def __init__( | |
| self.description = description | ||
| self.provider = provider | ||
| self.skills = skills or [] | ||
| self.extensions = extensions or [] | ||
| self.docs_url = docs_url | ||
| self.streaming = streaming | ||
| # NOTE: For now, I don't think there's any reason to support any other input/output modes. | ||
| self.default_input_modes = ['application/json'] | ||
| self.default_output_modes = ['application/json'] | ||
|
|
@@ -92,6 +97,11 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | |
|
|
||
| async def _agent_card_endpoint(self, request: Request) -> Response: | ||
| if self._agent_card_json_schema is None: | ||
| capabilities = AgentCapabilities( | ||
| streaming=self.streaming, push_notifications=False, state_transition_history=False | ||
| ) | ||
| if self.extensions: | ||
| capabilities['extensions'] = self.extensions | ||
| agent_card = AgentCard( | ||
| name=self.name, | ||
| description=self.description or 'An AI agent exposed as an A2A agent.', | ||
|
|
@@ -102,7 +112,7 @@ async def _agent_card_endpoint(self, request: Request) -> Response: | |
| skills=self.skills, | ||
| default_input_modes=self.default_input_modes, | ||
| default_output_modes=self.default_output_modes, | ||
| capabilities=AgentCapabilities(streaming=True, push_notifications=False), | ||
| capabilities=capabilities, | ||
| ) | ||
| if self.provider is not None: | ||
| agent_card['provider'] = self.provider | ||
|
|
@@ -130,9 +140,22 @@ async def _agent_run_endpoint(self, request: Request) -> Response: | |
| data = await request.body() | ||
| a2a_request = a2a_request_ta.validate_json(data) | ||
|
|
||
| # Parse activated extensions from the A2A-Extensions header | ||
| extensions_header = request.headers.get('a2a-extensions', '') | ||
| activated_extensions: list[str] = ( | ||
| [uri.strip() for uri in extensions_header.split(',') if uri.strip()] if extensions_header else [] | ||
| ) | ||
| # Stash on the request state so workers / handlers can inspect them | ||
| request.state.activated_extensions = activated_extensions | ||
|
Comment on lines
+143
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 activated_extensions parsed but never consumed by downstream code At Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| jsonrpc_response: A2AResponse | ||
| if a2a_request['method'] == 'message/send': | ||
| jsonrpc_response = await self.task_manager.send_message(a2a_request) | ||
| elif a2a_request['method'] == 'message/stream': | ||
| return StreamingResponse( | ||
| self.task_manager.stream_message(a2a_request), | ||
| media_type='text/event-stream', | ||
| ) | ||
|
Comment on lines
+154
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 No guard against The new Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| elif a2a_request['method'] == 'tasks/get': | ||
| jsonrpc_response = await self.task_manager.get_task(a2a_request) | ||
| elif a2a_request['method'] == 'tasks/cancel': | ||
|
|
@@ -147,11 +170,6 @@ async def _agent_run_endpoint(self, request: Request) -> Response: | |
| jsonrpc_response = await self.task_manager.delete_task_push_notification_config(a2a_request) | ||
| elif a2a_request['method'] == 'tasks/list': | ||
| jsonrpc_response = await self.task_manager.list_tasks(a2a_request) | ||
| elif a2a_request['method'] == 'message/stream': | ||
| return StreamingResponse( | ||
| self.task_manager.stream_message(a2a_request), | ||
| media_type='text/event-stream', | ||
| ) | ||
| elif a2a_request['method'] == 'tasks/resubscribe': | ||
| return StreamingResponse( | ||
| self.task_manager.resubscribe_task(a2a_request), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -90,6 +90,15 @@ class AgentCapabilities(TypedDict): | |||||||||||||||||
| extensions: NotRequired[list[AgentExtension]] | ||||||||||||||||||
| """A list of protocol extensions supported by the agent.""" | ||||||||||||||||||
|
|
||||||||||||||||||
| extensions: NotRequired[list[AgentExtension]] | ||||||||||||||||||
| """A2A extensions supported by this agent. | ||||||||||||||||||
|
|
||||||||||||||||||
| Each extension is declared as an ``AgentExtension`` object with a | ||||||||||||||||||
| unique ``uri``, optional ``description``, ``required`` flag, and | ||||||||||||||||||
| ``params`` configuration. Clients activate extensions by sending | ||||||||||||||||||
| the selected URIs in the ``A2A-Extensions`` HTTP header. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
Comment on lines
+93
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Duplicate The PR adds a second
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @pydantic.with_config({'alias_generator': to_camel}) | ||||||||||||||||||
| class HttpSecurityScheme(TypedDict): | ||||||||||||||||||
|
|
@@ -992,3 +1001,9 @@ class StreamResponse(TypedDict): | |||||||||||||||||
| send_message_response_ta: TypeAdapter[SendMessageResponse] = TypeAdapter(SendMessageResponse) | ||||||||||||||||||
| stream_message_request_ta: TypeAdapter[StreamMessageRequest] = TypeAdapter(StreamMessageRequest) | ||||||||||||||||||
| stream_message_response_ta: TypeAdapter[StreamMessageResponse] = TypeAdapter(StreamMessageResponse) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Type for streaming events (used by broker and task manager) | ||||||||||||||||||
| StreamEvent = Union[Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent] | ||||||||||||||||||
| """A streaming event that can be sent during message/stream requests.""" | ||||||||||||||||||
|
|
||||||||||||||||||
| stream_event_ta: TypeAdapter[StreamEvent] = TypeAdapter(StreamEvent) | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩
state_transition_history=Falseis silently dropped during serializationAt
fasta2a/applications.py:101,state_transition_history=Falseis passed to theAgentCapabilitiesTypedDict constructor, but this field is not defined in the TypedDict atfasta2a/schema.py:78-100. Pyright does flag this (confirmed viauv run pyright), so it's a CI-caught type error. However, the runtime impact is notable: pydantic'sdump_jsonsilently strips the unknown key, sostateTransitionHistorynever appears in the serialized agent card JSON. Since the value isFalse, this is arguably equivalent to omitting it (default assumed false), but if someone changes this toTruein the future, it would also be silently dropped. The fix would be to either add astate_transition_historyfield toAgentCapabilitiesTypedDict or remove it from the constructor call.Was this helpful? React with 👍 or 👎 to provide feedback.