diff --git a/.github/actions/spelling/line_forbidden.patterns b/.github/actions/spelling/line_forbidden.patterns index f87ad0b7..bc85a989 100644 --- a/.github/actions/spelling/line_forbidden.patterns +++ b/.github/actions/spelling/line_forbidden.patterns @@ -295,7 +295,7 @@ (?!'")[‘’“”] # "an" should only be before vowels. -\ban\s+(?![FHLMNRSX][A-Z0-9]+\b)(?!hour\b)(?!honest\b)([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]{1}\w*) +\ban\s+(?![FHLMNRSX][A-Z0-9]+\b)(?!hour\b)(?!honest\b)(?!httpx?\b)([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]{1}\w*) # Don't use Google internal links ((corp|prod|sandbox).google.com|googleplex.com|https?://(?!localhost/)[0-9a-z][0-9a-z-]+/|(?:^|[^/.-])\b(?:go|b|cl|cr)/[a-z0-9_.-]+\b) diff --git a/.github/linters/.mypy.ini b/.github/linters/.mypy.ini index 80ff63e5..88a66d54 100644 --- a/.github/linters/.mypy.ini +++ b/.github/linters/.mypy.ini @@ -1,6 +1,6 @@ [mypy] exclude = examples/ -disable_error_code = import-not-found +disable_error_code = import-not-found,annotation-unchecked [mypy-examples.*] follow_imports = skip diff --git a/src/a2a/__init__.py b/src/a2a/__init__.py index e69de29b..86893a97 100644 --- a/src/a2a/__init__.py +++ b/src/a2a/__init__.py @@ -0,0 +1 @@ +"""The A2A Python SDK.""" diff --git a/src/a2a/client/__init__.py b/src/a2a/client/__init__.py index 1be94a76..3455c867 100644 --- a/src/a2a/client/__init__.py +++ b/src/a2a/client/__init__.py @@ -1,3 +1,5 @@ +"""Client-side components for interacting with an A2A agent.""" + from a2a.client.client import A2ACardResolver, A2AClient from a2a.client.errors import ( A2AClientError, diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index 4703691e..24584c65 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -36,6 +36,13 @@ def __init__( base_url: str, agent_card_path: str = '/.well-known/agent.json', ): + """Initializes the A2ACardResolver. + + Args: + httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient). + base_url: The base URL of the agent's host. + agent_card_path: The path to the agent card endpoint, relative to the base URL. + """ self.base_url = base_url.rstrip('/') self.agent_card_path = agent_card_path.lstrip('/') self.httpx_client = httpx_client @@ -43,6 +50,20 @@ def __init__( async def get_agent_card( self, http_kwargs: dict[str, Any] | None = None ) -> AgentCard: + """Fetches the agent card from the specified URL. + + Args: + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.get request. + + Returns: + An `AgentCard` object representing the agent's capabilities. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON + or validated against the AgentCard schema. + """ try: response = await self.httpx_client.get( f'{self.base_url}/{self.agent_card_path}', @@ -62,7 +83,7 @@ async def get_agent_card( @trace_class(kind=SpanKind.CLIENT) class A2AClient: - """A2A Client.""" + """A2A Client for interacting with an A2A agent.""" def __init__( self, @@ -70,6 +91,18 @@ def __init__( agent_card: AgentCard | None = None, url: str | None = None, ): + """Initializes the A2AClient. + + Requires either an `AgentCard` or a direct `url` to the agent's RPC endpoint. + + Args: + httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient). + agent_card: The agent card object. If provided, `url` is taken from `agent_card.url`. + url: The direct URL to the agent's A2A RPC endpoint. Required if `agent_card` is None. + + Raises: + ValueError: If neither `agent_card` nor `url` is provided. + """ if agent_card: self.url = agent_card.url elif url: @@ -86,7 +119,22 @@ async def get_client_from_agent_card_url( agent_card_path: str = '/.well-known/agent.json', http_kwargs: dict[str, Any] | None = None, ) -> 'A2AClient': - """Get a A2A client for provided agent card URL.""" + """Fetches the AgentCard and initializes an A2A client. + + Args: + httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient). + base_url: The base URL of the agent's host. + agent_card_path: The path to the agent card endpoint, relative to the base URL. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.get request when fetching the agent card. + + Returns: + An initialized `A2AClient` instance. + + Raises: + A2AClientHTTPError: If an HTTP error occurs fetching the agent card. + A2AClientJSONError: If the agent card response is invalid. + """ agent_card: AgentCard = await A2ACardResolver( httpx_client, base_url=base_url, agent_card_path=agent_card_path ).get_agent_card(http_kwargs=http_kwargs) @@ -98,6 +146,20 @@ async def send_message( *, http_kwargs: dict[str, Any] | None = None, ) -> SendMessageResponse: + """Sends a non-streaming message request to the agent. + + Args: + request: The `SendMessageRequest` object containing the message and configuration. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + A `SendMessageResponse` object containing the agent's response (Task or Message) or an error. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) @@ -114,6 +176,23 @@ async def send_message_streaming( *, http_kwargs: dict[str, Any] | None = None, ) -> AsyncGenerator[SendStreamingMessageResponse]: + """Sends a streaming message request to the agent and yields responses as they arrive. + + This method uses Server-Sent Events (SSE) to receive a stream of updates from the agent. + + Args: + request: The `SendStreamingMessageRequest` object containing the message and configuration. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. A default `timeout=None` is set but can be overridden. + + Yields: + `SendStreamingMessageResponse` objects as they are received in the SSE stream. + These can be Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent. + + Raises: + A2AClientHTTPError: If an HTTP or SSE protocol error occurs during the request. + A2AClientJSONError: If an SSE event data cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) @@ -153,8 +232,16 @@ async def _send_request( """Sends a non-streaming JSON-RPC request to the agent. Args: - rpc_request_payload: JSON RPC payload for sending the request - **kwargs: Additional keyword arguments to pass to the httpx client. + rpc_request_payload: JSON RPC payload for sending the request. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + The JSON response payload as a dictionary. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON. """ try: response = await self.httpx_client.post( @@ -177,6 +264,20 @@ async def get_task( *, http_kwargs: dict[str, Any] | None = None, ) -> GetTaskResponse: + """Retrieves the current state and history of a specific task. + + Args: + request: The `GetTaskRequest` object specifying the task ID and history length. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + A `GetTaskResponse` object containing the Task or an error. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) @@ -193,6 +294,20 @@ async def cancel_task( *, http_kwargs: dict[str, Any] | None = None, ) -> CancelTaskResponse: + """Requests the agent to cancel a specific task. + + Args: + request: The `CancelTaskRequest` object specifying the task ID. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + A `CancelTaskResponse` object containing the updated Task with canceled status or an error. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) @@ -209,6 +324,20 @@ async def set_task_callback( *, http_kwargs: dict[str, Any] | None = None, ) -> SetTaskPushNotificationConfigResponse: + """Sets or updates the push notification configuration for a specific task. + + Args: + request: The `SetTaskPushNotificationConfigRequest` object specifying the task ID and configuration. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + A `SetTaskPushNotificationConfigResponse` object containing the confirmation or an error. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) @@ -225,6 +354,20 @@ async def get_task_callback( *, http_kwargs: dict[str, Any] | None = None, ) -> GetTaskPushNotificationConfigResponse: + """Retrieves the push notification configuration for a specific task. + + Args: + request: The `GetTaskPushNotificationConfigRequest` object specifying the task ID. + http_kwargs: Optional dictionary of keyword arguments to pass to the + underlying httpx.post request. + + Returns: + A `GetTaskPushNotificationConfigResponse` object containing the configuration or an error. + + Raises: + A2AClientHTTPError: If an HTTP error occurs during the request. + A2AClientJSONError: If the response body cannot be decoded as JSON or validated. + """ if not request.id: request.id = str(uuid4()) diff --git a/src/a2a/client/errors.py b/src/a2a/client/errors.py index b5e8d475..da02e582 100644 --- a/src/a2a/client/errors.py +++ b/src/a2a/client/errors.py @@ -1,19 +1,33 @@ +"""Custom exceptions for the A2A client.""" + + class A2AClientError(Exception): - """Base exception for client A2A Client errors.""" + """Base exception for A2A Client errors.""" class A2AClientHTTPError(A2AClientError): - """Client exception for HTTP errors.""" + """Client exception for HTTP errors received from the server.""" def __init__(self, status_code: int, message: str): + """Initializes the A2AClientHTTPError. + + Args: + status_code: The HTTP status code of the response. + message: A descriptive error message. + """ self.status_code = status_code self.message = message super().__init__(f'HTTP Error {status_code}: {message}') class A2AClientJSONError(A2AClientError): - """Client exception for JSON errors.""" + """Client exception for JSON errors during response parsing or validation.""" def __init__(self, message: str): + """Initializes the A2AClientJSONError. + + Args: + message: A descriptive error message. + """ self.message = message super().__init__(f'JSON Error: {message}') diff --git a/src/a2a/client/helpers.py b/src/a2a/client/helpers.py index ab728b73..4eedadb8 100644 --- a/src/a2a/client/helpers.py +++ b/src/a2a/client/helpers.py @@ -1,3 +1,5 @@ +"""Helper functions for the A2A client.""" + from uuid import uuid4 from a2a.types import Message, Part, Role, TextPart @@ -6,7 +8,15 @@ def create_text_message_object( role: Role = Role.user, content: str = '' ) -> Message: - """Create a Message object for the given role and content.""" + """Create a Message object containing a single TextPart. + + Args: + role: The role of the message sender (user or agent). Defaults to Role.user. + content: The text content of the message. Defaults to an empty string. + + Returns: + A `Message` object with a new UUID messageId. + """ return Message( role=role, parts=[Part(TextPart(text=content))], messageId=str(uuid4()) ) diff --git a/src/a2a/server/__init__.py b/src/a2a/server/__init__.py index e69de29b..6bd0c772 100644 --- a/src/a2a/server/__init__.py +++ b/src/a2a/server/__init__.py @@ -0,0 +1 @@ +"""Server-side components for implementing an A2A agent.""" diff --git a/src/a2a/server/agent_execution/__init__.py b/src/a2a/server/agent_execution/__init__.py index 88660d62..f6c853f6 100644 --- a/src/a2a/server/agent_execution/__init__.py +++ b/src/a2a/server/agent_execution/__init__.py @@ -1,3 +1,5 @@ +"""Components for executing agent logic within the A2A server.""" + from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext diff --git a/src/a2a/server/agent_execution/agent_executor.py b/src/a2a/server/agent_execution/agent_executor.py index bbf82a8b..489f752b 100644 --- a/src/a2a/server/agent_execution/agent_executor.py +++ b/src/a2a/server/agent_execution/agent_executor.py @@ -5,12 +5,36 @@ class AgentExecutor(ABC): - """Agent Executor interface.""" + """Agent Executor interface. + + Implementations of this interface contain the core logic of the agent, + executing tasks based on requests and publishing updates to an event queue. + """ @abstractmethod async def execute(self, context: RequestContext, event_queue: EventQueue): - pass + """Execute the agent's logic for a given request context. + + The agent should read necessary information from the `context` and + publish `Task` or `Message` events, or `TaskStatusUpdateEvent` / + `TaskArtifactUpdateEvent` to the `event_queue`. This method should + return once the agent's execution for this request is complete or + yields control (e.g., enters an input-required state). + + Args: + context: The request context containing the message, task ID, etc. + event_queue: The queue to publish events to. + """ @abstractmethod async def cancel(self, context: RequestContext, event_queue: EventQueue): - pass + """Request the agent to cancel an ongoing task. + + The agent should attempt to stop the task identified by the task_id + in the context and publish a `TaskStatusUpdateEvent` with state + `TaskState.canceled` to the `event_queue`. + + Args: + context: The request context containing the task ID to cancel. + event_queue: The queue to publish the cancellation status update to. + """ diff --git a/src/a2a/server/agent_execution/context.py b/src/a2a/server/agent_execution/context.py index 71169dd2..371b8419 100644 --- a/src/a2a/server/agent_execution/context.py +++ b/src/a2a/server/agent_execution/context.py @@ -12,7 +12,12 @@ class RequestContext: - """Request Context.""" + """Request Context. + + Holds information about the current request being processed by the server, + including the incoming message, task and context identifiers, and related + tasks. + """ def __init__( self, @@ -22,6 +27,15 @@ def __init__( task: Task | None = None, related_tasks: list[Task] | None = None, ): + """Initializes the RequestContext. + + Args: + request: The incoming `MessageSendParams` request payload. + task_id: The ID of the task explicitly provided in the request or path. + context_id: The ID of the context explicitly provided in the request or path. + task: The existing `Task` object retrieved from the store, if any. + related_tasks: A list of other tasks related to the current request (e.g., for tool use). + """ if related_tasks is None: related_tasks = [] self._params = request @@ -48,45 +62,71 @@ def __init__( self._check_or_generate_context_id() def get_user_input(self, delimiter='\n') -> str: + """Extracts text content from the user's message parts. + + Args: + delimiter: The string to use when joining multiple text parts. + + Returns: + A single string containing all text content from the user message, + joined by the specified delimiter. Returns an empty string if no + user message is present or if it contains no text parts. + """ if not self._params: return '' return get_message_text(self._params.message, delimiter) def attach_related_task(self, task: Task): + """Attaches a related task to the context. + + This is useful for scenarios like tool execution where a new task + might be spawned. + + Args: + task: The `Task` object to attach. + """ self._related_tasks.append(task) @property def message(self) -> Message | None: + """The incoming `Message` object from the request, if available.""" return self._params.message if self._params else None @property def related_tasks(self) -> list[Task]: + """A list of tasks related to the current request.""" return self._related_tasks @property def current_task(self) -> Task | None: + """The current `Task` object being processed.""" return self._current_task @current_task.setter def current_task(self, task: Task) -> None: + """Sets the current task object.""" self._current_task = task @property def task_id(self) -> str | None: + """The ID of the task associated with this context.""" return self._task_id @property def context_id(self) -> str | None: + """The ID of the conversation context associated with this task.""" return self._context_id @property def configuration(self) -> MessageSendConfiguration | None: + """The `MessageSendConfiguration` from the request, if available.""" if not self._params: return None return self._params.configuration def _check_or_generate_task_id(self) -> None: + """Ensures a task ID is present, generating one if necessary.""" if not self._params: return @@ -96,6 +136,7 @@ def _check_or_generate_task_id(self) -> None: self._task_id = self._params.message.taskId def _check_or_generate_context_id(self) -> None: + """Ensures a context ID is present, generating one if necessary.""" if not self._params: return diff --git a/src/a2a/server/apps/__init__.py b/src/a2a/server/apps/__init__.py index 76b6e465..7265c403 100644 --- a/src/a2a/server/apps/__init__.py +++ b/src/a2a/server/apps/__init__.py @@ -1,3 +1,5 @@ +"""HTTP application components for the A2A server.""" + from a2a.server.apps.http_app import HttpApp from a2a.server.apps.starlette_app import A2AStarletteApplication diff --git a/src/a2a/server/apps/http_app.py b/src/a2a/server/apps/http_app.py index a04cb247..576584e7 100644 --- a/src/a2a/server/apps/http_app.py +++ b/src/a2a/server/apps/http_app.py @@ -5,8 +5,20 @@ class HttpApp(ABC): - """A2A Server application interface.""" + """A2A Server application interface. + + Defines the interface for building an HTTP application that serves + the A2A protocol. + """ @abstractmethod def build(self, **kwargs: Any) -> Starlette: - pass + """Builds and returns a Starlette application instance. + + Args: + **kwargs: Additional keyword arguments to pass to the Starlette + constructor. + + Returns: + A configured Starlette application instance. + """ diff --git a/src/a2a/server/apps/starlette_app.py b/src/a2a/server/apps/starlette_app.py index 5bbdcced..f9478a24 100644 --- a/src/a2a/server/apps/starlette_app.py +++ b/src/a2a/server/apps/starlette_app.py @@ -51,7 +51,7 @@ class A2AStarletteApplication: """ def __init__(self, agent_card: AgentCard, http_handler: RequestHandler): - """Initializes the A2AApplication. + """Initializes the A2AStarletteApplication. Args: agent_card: The AgentCard describing the agent's capabilities. @@ -66,7 +66,17 @@ def __init__(self, agent_card: AgentCard, http_handler: RequestHandler): def _generate_error_response( self, request_id: str | int | None, error: JSONRPCError | A2AError ) -> JSONResponse: - """Creates a JSONResponse for a JSON-RPC error.""" + """Creates a Starlette JSONResponse for a JSON-RPC error. + + Logs the error based on its type. + + Args: + request_id: The ID of the request that caused the error. + error: The `JSONRPCError` or `A2AError` object. + + Returns: + A `JSONResponse` object formatted as a JSON-RPC error response. + """ error_resp = JSONRPCErrorResponse( id=request_id, error=error if isinstance(error, JSONRPCError) else error.root, @@ -80,7 +90,7 @@ def _generate_error_response( ) logger.log( log_level, - f'Request Error (ID: {request_id}: ' + f'Request Error (ID: {request_id}): ' f"Code={error_resp.error.code}, Message='{error_resp.error.message}'" f'{", Data=" + str(error_resp.error.data) if hasattr(error, "data") and error_resp.error.data else ""}', ) @@ -96,6 +106,16 @@ async def _handle_requests(self, request: Request) -> Response: dispatches it to the appropriate handler method, and returns the response. Handles JSON parsing errors, validation errors, and other exceptions, returning appropriate JSON-RPC error responses. + + Args: + request: The incoming Starlette Request object. + + Returns: + A Starlette Response object (JSONResponse or EventSourceResponse). + + Raises: + (Implicitly handled): Various exceptions are caught and converted + into JSON-RPC error responses by this method. """ request_id = None body = None @@ -144,11 +164,14 @@ async def _handle_requests(self, request: Request) -> Response: async def _process_streaming_request( self, request_id: str | int | None, a2a_request: A2ARequest ) -> Response: - """Processes streaming requests. + """Processes streaming requests (message/stream or tasks/resubscribe). Args: request_id: The ID of the request. a2a_request: The validated A2ARequest object. + + Returns: + An `EventSourceResponse` object to stream results to the client. """ request_obj = a2a_request.root handler_result: Any = None @@ -165,11 +188,14 @@ async def _process_streaming_request( async def _process_non_streaming_request( self, request_id: str | int | None, a2a_request: A2ARequest ) -> Response: - """Processes non-streaming requests. + """Processes non-streaming requests (message/send, tasks/get, tasks/cancel, tasks/pushNotificationConfig/*). Args: request_id: The ID of the request. a2a_request: The validated A2ARequest object. + + Returns: + A `JSONResponse` object containing the result or error. """ request_obj = a2a_request.root handler_result: Any = None @@ -204,7 +230,7 @@ async def _process_non_streaming_request( def _create_response( self, handler_result: ( - AsyncGenerator[SendStreamingMessageResponse, None] + AsyncGenerator[SendStreamingMessageResponse] | JSONRPCErrorResponse | JSONRPCResponse ), @@ -216,10 +242,10 @@ def _create_response( - JSONRPCErrorResponse for explicit errors returned by handlers. - Pydantic RootModels (like GetTaskResponse) containing success or error payloads. - - Unexpected types by returning an InternalError. Args: - handler_result: AsyncGenerator of SendStreamingMessageResponse + handler_result: The result from a request handler method. Can be an + async generator for streaming or a Pydantic model for non-streaming. Returns: A Starlette JSONResponse or EventSourceResponse. @@ -227,8 +253,8 @@ def _create_response( if isinstance(handler_result, AsyncGenerator): # Result is a stream of SendStreamingMessageResponse objects async def event_generator( - stream: AsyncGenerator[SendStreamingMessageResponse, None], - ) -> AsyncGenerator[dict[str, str], None]: + stream: AsyncGenerator[SendStreamingMessageResponse], + ) -> AsyncGenerator[dict[str, str]]: async for item in stream: yield {'data': item.root.model_dump_json(exclude_none=True)} @@ -246,7 +272,14 @@ async def event_generator( ) async def _handle_get_agent_card(self, request: Request) -> JSONResponse: - """Handles GET requests for the agent card.""" + """Handles GET requests for the agent card endpoint. + + Args: + request: The incoming Starlette Request object. + + Returns: + A JSONResponse containing the agent card data. + """ return JSONResponse( self.agent_card.model_dump(mode='json', exclude_none=True) ) @@ -259,11 +292,11 @@ def routes( """Returns the Starlette Routes for handling A2A requests. Args: - agent_card_url: The URL for the agent card endpoint. - rpc_url: The URL for the A2A JSON-RPC endpoint + agent_card_url: The URL path for the agent card endpoint. + rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests). Returns: - The Starlette Routes serving A2A requests. + A list of Starlette Route objects. """ return [ Route( @@ -289,8 +322,8 @@ def build( """Builds and returns the Starlette application instance. Args: - agent_card_url: The URL for the agent card endpoint. - rpc_url: The URL for the A2A JSON-RPC endpoint + agent_card_url: The URL path for the agent card endpoint. + rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests). **kwargs: Additional keyword arguments to pass to the Starlette constructor. diff --git a/src/a2a/server/events/__init__.py b/src/a2a/server/events/__init__.py index d9662d04..64f6da21 100644 --- a/src/a2a/server/events/__init__.py +++ b/src/a2a/server/events/__init__.py @@ -1,3 +1,5 @@ +"""Event handling components for the A2A server.""" + from a2a.server.events.event_consumer import EventConsumer from a2a.server.events.event_queue import Event, EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager diff --git a/src/a2a/server/events/event_consumer.py b/src/a2a/server/events/event_consumer.py index 03ff278a..d963adf3 100644 --- a/src/a2a/server/events/event_consumer.py +++ b/src/a2a/server/events/event_consumer.py @@ -23,13 +23,26 @@ class EventConsumer: """Consumer to read events from the agent event queue.""" def __init__(self, queue: EventQueue): + """Initializes the EventConsumer. + + Args: + queue: The `EventQueue` instance to consume events from. + """ self.queue = queue self._timeout = 0.5 self._exception: BaseException | None = None logger.debug('EventConsumer initialized') async def consume_one(self) -> Event: - """Consume one event from the agent event queue.""" + """Consume one event from the agent event queue non-blocking. + + Returns: + The next event from the queue. + + Raises: + ServerError: If the queue is empty when attempting to dequeue + immediately. + """ logger.debug('Attempting to consume one event.') try: event = await self.queue.dequeue_event(no_wait=True) @@ -46,7 +59,18 @@ async def consume_one(self) -> Event: return event async def consume_all(self) -> AsyncGenerator[Event]: - """Consume all the generated streaming events from the agent.""" + """Consume all the generated streaming events from the agent. + + This method yields events as they become available from the queue + until a final event is received or the queue is closed. It also + monitors for exceptions set by the `agent_task_callback`. + + Yields: + Events dequeued from the queue. + + Raises: + BaseException: If an exception was set by the `agent_task_callback`. + """ logger.debug('Starting to consume all events from the queue.') while True: if self._exception: @@ -96,5 +120,14 @@ async def consume_all(self) -> AsyncGenerator[Event]: break def agent_task_callback(self, agent_task: asyncio.Task[None]): + """Callback to handle exceptions from the agent's execution task. + + If the agent's asyncio task raises an exception, this callback is + invoked, and the exception is stored to be re-raised by the consumer loop. + + Args: + agent_task: The asyncio.Task that completed. + """ + logger.debug('Agent task callback triggered.') if agent_task.exception() is not None: self._exception = agent_task.exception() diff --git a/src/a2a/server/events/event_queue.py b/src/a2a/server/events/event_queue.py index fbe63822..10c9ec0c 100644 --- a/src/a2a/server/events/event_queue.py +++ b/src/a2a/server/events/event_queue.py @@ -1,8 +1,6 @@ import asyncio import logging -from typing import Any - from a2a.types import ( A2AError, JSONRPCError, @@ -25,24 +23,49 @@ | A2AError | JSONRPCError ) +"""Type alias for events that can be enqueued.""" @trace_class(kind=SpanKind.SERVER) class EventQueue: - """Event queue for A2A responses from agent.""" + """Event queue for A2A responses from agent. + + Acts as a buffer between the agent's asynchronous execution and the + server's response handling (e.g., streaming via SSE). Supports tapping + to create child queues that receive the same events. + """ def __init__(self) -> None: + """Initializes the EventQueue.""" self.queue: asyncio.Queue[Event] = asyncio.Queue() self._children: list[EventQueue] = [] logger.debug('EventQueue initialized.') def enqueue_event(self, event: Event): + """Enqueues an event to this queue and all its children. + + Args: + event: The event object to enqueue. + """ logger.debug(f'Enqueuing event of type: {type(event)}') self.queue.put_nowait(event) for child in self._children: child.enqueue_event(event) async def dequeue_event(self, no_wait: bool = False) -> Event: + """Dequeues an event from the queue. + + Args: + no_wait: If True, retrieve an event immediately or raise `asyncio.QueueEmpty`. + If False (default), wait until an event is available. + + Returns: + The next event from the queue. + + Raises: + asyncio.QueueEmpty: If `no_wait` is True and the queue is empty. + asyncio.QueueShutDown: If the queue has been closed and is empty. + """ if no_wait: logger.debug('Attempting to dequeue event (no_wait=True).') event = self.queue.get_nowait() @@ -57,17 +80,32 @@ async def dequeue_event(self, no_wait: bool = False) -> Event: return event def task_done(self) -> None: + """Signals that a formerly enqueued task is complete. + + Used in conjunction with `dequeue_event` to track processed items. + """ logger.debug('Marking task as done in EventQueue.') self.queue.task_done() - def tap(self) -> Any: - """Taps the event queue to branch the future events.""" + def tap(self) -> 'EventQueue': + """Taps the event queue to create a new child queue that receives all future events. + + Returns: + A new `EventQueue` instance that will receive all events enqueued + to this parent queue from this point forward. + """ + logger.debug('Tapping EventQueue to create a child queue.') queue = EventQueue() self._children.append(queue) return queue def close(self): - """Closes the queue for future push events.""" + """Closes the queue for future push events. + + Once closed, `dequeue_event` will eventually raise `asyncio.QueueShutDown` + when the queue is empty. Also closes all child queues. + """ + logger.debug('Closing EventQueue.') self.queue.shutdown() for child in self._children: child.close() diff --git a/src/a2a/server/events/in_memory_queue_manager.py b/src/a2a/server/events/in_memory_queue_manager.py index c2b07937..db7663c4 100644 --- a/src/a2a/server/events/in_memory_queue_manager.py +++ b/src/a2a/server/events/in_memory_queue_manager.py @@ -13,42 +13,70 @@ class InMemoryQueueManager(QueueManager): """InMemoryQueueManager is used for a single binary management. - This implements the QueueManager but requires all incoming interactions - to hit the same binary that manages the queues. + This implements the `QueueManager` interface using in-memory storage for event + queues. It requires all incoming interactions for a given task ID to hit the + same binary instance. - This works for single binary solution. Needs a distributed approach for - true scalable deployment. + This implementation is suitable for single-instance deployments but needs + a distributed approach for scalable deployments. """ def __init__(self) -> None: + """Initializes the InMemoryQueueManager.""" self._task_queue: dict[str, EventQueue] = {} self._lock = asyncio.Lock() async def add(self, task_id: str, queue: EventQueue): + """Adds a new event queue for a task ID. + + Raises: + TaskQueueExists: If a queue for the given `task_id` already exists. + """ async with self._lock: if task_id in self._task_queue: raise TaskQueueExists() self._task_queue[task_id] = queue async def get(self, task_id: str) -> EventQueue | None: + """Retrieves the event queue for a task ID. + + Returns: + The `EventQueue` instance for the `task_id`, or `None` if not found. + """ async with self._lock: if task_id not in self._task_queue: return None return self._task_queue[task_id] async def tap(self, task_id: str) -> EventQueue | None: + """Taps the event queue for a task ID to create a child queue. + + Returns: + A new child `EventQueue` instance, or `None` if the task ID is not found. + """ async with self._lock: if task_id not in self._task_queue: return None return self._task_queue[task_id].tap() async def close(self, task_id: str): + """Closes and removes the event queue for a task ID. + + Raises: + NoTaskQueue: If no queue exists for the given `task_id`. + """ async with self._lock: if task_id not in self._task_queue: raise NoTaskQueue() - del self._task_queue[task_id] + queue = self._task_queue.pop(task_id) + queue.close() async def create_or_tap(self, task_id: str) -> EventQueue: + """Creates a new event queue for a task ID if one doesn't exist, otherwise taps the existing one. + + Returns: + A new or child `EventQueue` instance for the `task_id`. + """ async with self._lock: if task_id not in self._task_queue: queue = EventQueue() diff --git a/src/a2a/server/events/queue_manager.py b/src/a2a/server/events/queue_manager.py index bef273f3..7330a097 100644 --- a/src/a2a/server/events/queue_manager.py +++ b/src/a2a/server/events/queue_manager.py @@ -4,32 +4,32 @@ class QueueManager(ABC): - """Interface for managing the event queue lifecycles.""" + """Interface for managing the event queue lifecycles per task.""" @abstractmethod async def add(self, task_id: str, queue: EventQueue): - pass + """Adds a new event queue associated with a task ID.""" @abstractmethod async def get(self, task_id: str) -> EventQueue | None: - pass + """Retrieves the event queue for a task ID.""" @abstractmethod async def tap(self, task_id: str) -> EventQueue | None: - pass + """Creates a child event queue (tap) for an existing task ID.""" @abstractmethod async def close(self, task_id: str): - pass + """Closes and removes the event queue for a task ID.""" @abstractmethod async def create_or_tap(self, task_id: str) -> EventQueue: - pass + """Creates a queue if one doesn't exist, otherwise taps the existing one.""" class TaskQueueExists(Exception): - pass + """Exception raised when attempting to add a queue for a task ID that already exists.""" class NoTaskQueue(Exception): - pass + """Exception raised when attempting to access or close a queue for a task ID that does not exist.""" diff --git a/src/a2a/server/request_handlers/__init__.py b/src/a2a/server/request_handlers/__init__.py index 9b3851ce..f0d2667d 100644 --- a/src/a2a/server/request_handlers/__init__.py +++ b/src/a2a/server/request_handlers/__init__.py @@ -1,3 +1,5 @@ +"""Request handler components for the A2A server.""" + from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 17cbd49f..e3a47355 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -42,7 +42,12 @@ @trace_class(kind=SpanKind.SERVER) class DefaultRequestHandler(RequestHandler): - """Default request handler for all incoming requests.""" + """Default request handler for all incoming requests. + + This handler provides default implementations for all A2A JSON-RPC methods, + coordinating between the `AgentExecutor`, `TaskStore`, `QueueManager`, + and optional `PushNotifier`. + """ _running_agents: dict[str, asyncio.Task] @@ -53,6 +58,14 @@ def __init__( queue_manager: QueueManager | None = None, push_notifier: PushNotifier | None = None, ) -> None: + """Initializes the DefaultRequestHandler. + + Args: + agent_executor: The `AgentExecutor` instance to run agent logic. + task_store: The `TaskStore` instance to manage task persistence. + queue_manager: The `QueueManager` instance to manage event queues. Defaults to `InMemoryQueueManager`. + push_notifier: The `PushNotifier` instance for sending push notifications. Defaults to None. + """ self.agent_executor = agent_executor self.task_store = task_store self._queue_manager = queue_manager or InMemoryQueueManager() @@ -69,7 +82,10 @@ async def on_get_task(self, params: TaskQueryParams) -> Task | None: return task async def on_cancel_task(self, params: TaskIdParams) -> Task | None: - """Default handler for 'tasks/cancel'.""" + """Default handler for 'tasks/cancel'. + + Attempts to cancel the task managed by the `AgentExecutor`. + """ task: Task | None = await self.task_store.get(params.id) if not task: raise ServerError(error=TaskNotFoundError()) @@ -105,19 +121,31 @@ async def on_cancel_task(self, params: TaskIdParams) -> Task | None: return result raise ServerError( - error=InternalError(message='Agent did not result valid response') + error=InternalError( + message='Agent did not return valid response for cancel' + ) ) async def _run_event_stream( self, request: RequestContext, queue: EventQueue ) -> None: + """Runs the agent's `execute` method and closes the queue afterwards. + + Args: + request: The request context for the agent. + queue: The event queue for the agent to publish to. + """ await self.agent_executor.execute(request, queue) queue.close() async def on_message_send( self, params: MessageSendParams ) -> Message | Task: - """Default handler for 'message/send' interface.""" + """Default handler for 'message/send' interface (non-streaming). + + Starts the agent execution for the message and waits for the final + result (Task or Message). + """ task_manager = TaskManager( task_id=params.message.taskId, context_id=params.message.contextId, @@ -185,7 +213,11 @@ async def on_message_send( async def on_message_send_stream( self, params: MessageSendParams ) -> AsyncGenerator[Event]: - """Default handler for 'message/stream'.""" + """Default handler for 'message/stream' (streaming). + + Starts the agent execution and yields events as they are produced + by the agent. + """ task_manager = TaskManager( task_id=params.message.taskId, context_id=params.message.contextId, @@ -261,11 +293,19 @@ async def on_message_send_stream( finally: await self._cleanup_producer(producer_task, task_id) - async def _register_producer(self, task_id, producer_task) -> None: + async def _register_producer( + self, task_id: str, producer_task: asyncio.Task + ) -> None: + """Registers the agent execution task with the handler.""" async with self._running_agents_lock: self._running_agents[task_id] = producer_task - async def _cleanup_producer(self, producer_task, task_id) -> None: + async def _cleanup_producer( + self, + producer_task: asyncio.Task, + task_id: str, + ) -> None: + """Cleans up the agent execution task and queue manager entry.""" await producer_task await self._queue_manager.close(task_id) async with self._running_agents_lock: @@ -274,7 +314,10 @@ async def _cleanup_producer(self, producer_task, task_id) -> None: async def on_set_task_push_notification_config( self, params: TaskPushNotificationConfig ) -> TaskPushNotificationConfig: - """Default handler for 'tasks/pushNotificationConfig/set'.""" + """Default handler for 'tasks/pushNotificationConfig/set'. + + Requires a `PushNotifier` to be configured. + """ if not self._push_notifier: raise ServerError(error=UnsupportedOperationError()) @@ -292,7 +335,10 @@ async def on_set_task_push_notification_config( async def on_get_task_push_notification_config( self, params: TaskIdParams ) -> TaskPushNotificationConfig: - """Default handler for 'tasks/pushNotificationConfig/get'.""" + """Default handler for 'tasks/pushNotificationConfig/get'. + + Requires a `PushNotifier` to be configured. + """ if not self._push_notifier: raise ServerError(error=UnsupportedOperationError()) @@ -311,7 +357,11 @@ async def on_get_task_push_notification_config( async def on_resubscribe_to_task( self, params: TaskIdParams ) -> AsyncGenerator[Event]: - """Default handler for 'tasks/resubscribe'.""" + """Default handler for 'tasks/resubscribe'. + + Allows a client to re-attach to a running streaming task's event stream. + Requires the task and its queue to still be active. + """ task: Task | None = await self.task_store.get(params.id) if not task: raise ServerError(error=TaskNotFoundError()) diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 0475ab12..c766d999 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -44,26 +44,34 @@ @trace_class(kind=SpanKind.SERVER) class JSONRPCHandler: - """Maps the JSONRPC Objects to the request handler and back.""" + """Maps incoming JSON-RPC requests to the appropriate request handler method and formats responses.""" def __init__( self, agent_card: AgentCard, request_handler: RequestHandler, ): - """Initializes the HttpProducer. + """Initializes the JSONRPCHandler. Args: agent_card: The AgentCard describing the agent's capabilities. - request_handler: The handler instance to process A2A requests. + request_handler: The underlying `RequestHandler` instance to delegate requests to. """ self.agent_card = agent_card self.request_handler = request_handler - # message/send async def on_message_send( self, request: SendMessageRequest ) -> SendMessageResponse: + """Handles the 'message/send' JSON-RPC method. + + Args: + request: The incoming `SendMessageRequest` object. + + Returns: + A `SendMessageResponse` object containing the result (Task or Message) + or a JSON-RPC error response if a `ServerError` is raised by the handler. + """ # TODO: Wrap in error handler to return error states try: task_or_message = await self.request_handler.on_message_send( @@ -83,7 +91,6 @@ async def on_message_send( ) ) - # message/stream @validate( lambda self: self.agent_card.capabilities.streaming, 'Streaming is not supported by the agent', @@ -91,6 +98,18 @@ async def on_message_send( async def on_message_send_stream( self, request: SendStreamingMessageRequest ) -> AsyncIterable[SendStreamingMessageResponse]: + """Handles the 'message/stream' JSON-RPC method. + + Yields response objects as they are produced by the underlying handler's stream. + + Args: + request: The incoming `SendStreamingMessageRequest` object. + + Yields: + `SendStreamingMessageResponse` objects containing streaming events + (Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent) + or JSON-RPC error responses if a `ServerError` is raised. + """ try: async for event in self.request_handler.on_message_send_stream( request.params @@ -114,10 +133,17 @@ async def on_message_send_stream( ) ) - # tasks/cancel async def on_cancel_task( self, request: CancelTaskRequest ) -> CancelTaskResponse: + """Handles the 'tasks/cancel' JSON-RPC method. + + Args: + request: The incoming `CancelTaskRequest` object. + + Returns: + A `CancelTaskResponse` object containing the updated Task or a JSON-RPC error. + """ try: task = await self.request_handler.on_cancel_task(request.params) if task: @@ -136,10 +162,20 @@ async def on_cancel_task( ) ) - # tasks/resubscribe async def on_resubscribe_to_task( self, request: TaskResubscriptionRequest ) -> AsyncIterable[SendStreamingMessageResponse]: + """Handles the 'tasks/resubscribe' JSON-RPC method. + + Yields response objects as they are produced by the underlying handler's stream. + + Args: + request: The incoming `TaskResubscriptionRequest` object. + + Yields: + `SendStreamingMessageResponse` objects containing streaming events + or JSON-RPC error responses if a `ServerError` is raised. + """ try: async for event in self.request_handler.on_resubscribe_to_task( request.params @@ -163,10 +199,17 @@ async def on_resubscribe_to_task( ) ) - # tasks/pushNotification/get async def get_push_notification( self, request: GetTaskPushNotificationConfigRequest ) -> GetTaskPushNotificationConfigResponse: + """Handles the 'tasks/pushNotificationConfig/get' JSON-RPC method. + + Args: + request: The incoming `GetTaskPushNotificationConfigRequest` object. + + Returns: + A `GetTaskPushNotificationConfigResponse` object containing the config or a JSON-RPC error. + """ try: config = ( await self.request_handler.on_get_task_push_notification_config( @@ -187,7 +230,6 @@ async def get_push_notification( ) ) - # tasks/pushNotification/set @validate( lambda self: self.agent_card.capabilities.pushNotifications, 'Push notifications are not supported by the agent', @@ -195,6 +237,20 @@ async def get_push_notification( async def set_push_notification( self, request: SetTaskPushNotificationConfigRequest ) -> SetTaskPushNotificationConfigResponse: + """Handles the 'tasks/pushNotificationConfig/set' JSON-RPC method. + + Requires the agent to support push notifications. + + Args: + request: The incoming `SetTaskPushNotificationConfigRequest` object. + + Returns: + A `SetTaskPushNotificationConfigResponse` object containing the config or a JSON-RPC error. + + Raises: + ServerError: If push notifications are not supported by the agent + (due to the `@validate` decorator). + """ try: config = ( await self.request_handler.on_set_task_push_notification_config( @@ -215,8 +271,15 @@ async def set_push_notification( ) ) - # tasks/get async def on_get_task(self, request: GetTaskRequest) -> GetTaskResponse: + """Handles the 'tasks/get' JSON-RPC method. + + Args: + request: The incoming `GetTaskRequest` object. + + Returns: + A `GetTaskResponse` object containing the Task or a JSON-RPC error. + """ try: task = await self.request_handler.on_get_task(request.params) if task: diff --git a/src/a2a/server/request_handlers/request_handler.py b/src/a2a/server/request_handlers/request_handler.py index d7b37d76..a8229a8a 100644 --- a/src/a2a/server/request_handlers/request_handler.py +++ b/src/a2a/server/request_handlers/request_handler.py @@ -15,26 +15,72 @@ class RequestHandler(ABC): - """A2A request handler interface.""" + """A2A request handler interface. + + This interface defines the methods that an A2A server implementation must + provide to handle incoming JSON-RPC requests. + """ @abstractmethod async def on_get_task(self, params: TaskQueryParams) -> Task | None: - pass + """Handles the 'tasks/get' method. + + Retrieves the state and history of a specific task. + + Args: + params: Parameters specifying the task ID and optionally history length. + + Returns: + The `Task` object if found, otherwise `None`. + """ @abstractmethod async def on_cancel_task(self, params: TaskIdParams) -> Task | None: - pass + """Handles the 'tasks/cancel' method. + + Requests the agent to cancel an ongoing task. + + Args: + params: Parameters specifying the task ID. + + Returns: + The `Task` object with its status updated to canceled, or `None` if the task was not found. + """ @abstractmethod async def on_message_send( self, params: MessageSendParams ) -> Task | Message: - pass + """Handles the 'message/send' method (non-streaming). + + Sends a message to the agent to create, continue, or restart a task, + and waits for the final result (Task or Message). + + Args: + params: Parameters including the message and configuration. + + Returns: + The final `Task` object or a final `Message` object. + """ @abstractmethod async def on_message_send_stream( self, params: MessageSendParams - ) -> AsyncGenerator[Event, None]: + ) -> AsyncGenerator[Event]: + """Handles the 'message/stream' method (streaming). + + Sends a message to the agent and yields stream events as they are + produced (Task updates, Message chunks, Artifact updates). + + Args: + params: Parameters including the message and configuration. + + Yields: + `Event` objects from the agent's execution. + + Raises: + ServerError(UnsupportedOperationError): By default, if not implemented. + """ raise ServerError(error=UnsupportedOperationError()) yield @@ -42,17 +88,48 @@ async def on_message_send_stream( async def on_set_task_push_notification_config( self, params: TaskPushNotificationConfig ) -> TaskPushNotificationConfig: - pass + """Handles the 'tasks/pushNotificationConfig/set' method. + + Sets or updates the push notification configuration for a task. + + Args: + params: Parameters including the task ID and push notification configuration. + + Returns: + The provided `TaskPushNotificationConfig` upon success. + """ @abstractmethod async def on_get_task_push_notification_config( self, params: TaskIdParams ) -> TaskPushNotificationConfig: - pass + """Handles the 'tasks/pushNotificationConfig/get' method. + + Retrieves the current push notification configuration for a task. + + Args: + params: Parameters including the task ID. + + Returns: + The `TaskPushNotificationConfig` for the task. + """ @abstractmethod async def on_resubscribe_to_task( self, params: TaskIdParams - ) -> AsyncGenerator[Event, None]: + ) -> AsyncGenerator[Event]: + """Handles the 'tasks/resubscribe' method. + + Allows a client to re-subscribe to a running streaming task's event stream. + + Args: + params: Parameters including the task ID. + + Yields: + `Event` objects from the agent's ongoing execution for the specified task. + + Raises: + ServerError(UnsupportedOperationError): By default, if not implemented. + """ raise ServerError(error=UnsupportedOperationError()) yield diff --git a/src/a2a/server/request_handlers/response_helpers.py b/src/a2a/server/request_handlers/response_helpers.py index 3af1c2eb..b4e48ad9 100644 --- a/src/a2a/server/request_handlers/response_helpers.py +++ b/src/a2a/server/request_handlers/response_helpers.py @@ -1,3 +1,5 @@ +"""Helper functions for building A2A JSON-RPC responses.""" + # response types from typing import TypeVar @@ -35,6 +37,7 @@ GetTaskPushNotificationConfigResponse, SendStreamingMessageResponse, ) +"""Type variable for RootModel response types.""" # success types SPT = TypeVar( @@ -46,6 +49,7 @@ GetTaskPushNotificationConfigSuccessResponse, SendStreamingMessageSuccessResponse, ) +"""Type variable for SuccessResponse types.""" # result types EventTypes = ( @@ -57,6 +61,7 @@ | A2AError | JSONRPCError ) +"""Type alias for possible event types produced by handlers.""" def build_error_response( @@ -64,7 +69,18 @@ def build_error_response( error: A2AError | JSONRPCError, response_wrapper_type: type[RT], ) -> RT: - """Helper method to build a JSONRPCErrorResponse.""" + """Helper method to build a JSONRPCErrorResponse wrapped in the appropriate response type. + + Args: + request_id: The ID of the request that caused the error. + error: The A2AError or JSONRPCError object. + response_wrapper_type: The Pydantic RootModel type that wraps the response + for the specific RPC method (e.g., `SendMessageResponse`). + + Returns: + A Pydantic model representing the JSON-RPC error response, + wrapped in the specified response type. + """ return response_wrapper_type( JSONRPCErrorResponse( id=request_id, @@ -80,7 +96,24 @@ def prepare_response_object( success_payload_type: type[SPT], response_type: type[RT], ) -> RT: - """Helper method to build appropriate JSONRPCResponse object for RPC methods.""" + """Helper method to build appropriate JSONRPCResponse object for RPC methods. + + Based on the type of the `response` object received from the handler, + it constructs either a success response wrapped in the appropriate payload type + or an error response. + + Args: + request_id: The ID of the request. + response: The object received from the request handler. + success_response_types: A tuple of expected Pydantic model types for a successful result. + success_payload_type: The Pydantic model type for the success payload + (e.g., `SendMessageSuccessResponse`). + response_type: The Pydantic RootModel type that wraps the final response + (e.g., `SendMessageResponse`). + + Returns: + A Pydantic model representing the final JSON-RPC response (success or error). + """ if isinstance(response, success_response_types): return response_type( root=success_payload_type(id=request_id, result=response) # type:ignore diff --git a/src/a2a/server/tasks/__init__.py b/src/a2a/server/tasks/__init__.py index 4dc94947..ab8f52f0 100644 --- a/src/a2a/server/tasks/__init__.py +++ b/src/a2a/server/tasks/__init__.py @@ -1,3 +1,5 @@ +"""Components for managing tasks within the A2A server.""" + from a2a.server.tasks.inmemory_push_notifier import InMemoryPushNotifier from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from a2a.server.tasks.push_notifier import PushNotifier diff --git a/src/a2a/server/tasks/inmemory_push_notifier.py b/src/a2a/server/tasks/inmemory_push_notifier.py index 222dc90f..7c682901 100644 --- a/src/a2a/server/tasks/inmemory_push_notifier.py +++ b/src/a2a/server/tasks/inmemory_push_notifier.py @@ -11,9 +11,18 @@ class InMemoryPushNotifier(PushNotifier): - """In-memory implementation of PushNotifier interface.""" + """In-memory implementation of PushNotifier interface. + + Stores push notification configurations in memory and uses an httpx client + to send notifications. + """ def __init__(self, httpx_client: httpx.AsyncClient) -> None: + """Initializes the InMemoryPushNotifier. + + Args: + httpx_client: An async HTTP client instance to send notifications. + """ self._client = httpx_client self.lock = asyncio.Lock() self._push_notification_infos: dict[str, PushNotificationConfig] = {} @@ -21,19 +30,23 @@ def __init__(self, httpx_client: httpx.AsyncClient) -> None: async def set_info( self, task_id: str, notification_config: PushNotificationConfig ): + """Sets or updates the push notification configuration for a task in memory.""" async with self.lock: self._push_notification_infos[task_id] = notification_config async def get_info(self, task_id: str) -> PushNotificationConfig | None: + """Retrieves the push notification configuration for a task from memory.""" async with self.lock: return self._push_notification_infos.get(task_id) async def delete_info(self, task_id: str): + """Deletes the push notification configuration for a task from memory.""" async with self.lock: if task_id in self._push_notification_infos: del self._push_notification_infos[task_id] async def send_notification(self, task: Task): + """Sends a push notification for a task if configuration exists.""" push_info = await self.get_info(task.id) if not push_info: return diff --git a/src/a2a/server/tasks/inmemory_task_store.py b/src/a2a/server/tasks/inmemory_task_store.py index b3053e87..4e43dfec 100644 --- a/src/a2a/server/tasks/inmemory_task_store.py +++ b/src/a2a/server/tasks/inmemory_task_store.py @@ -9,19 +9,26 @@ class InMemoryTaskStore(TaskStore): - """In-memory implementation of TaskStore.""" + """In-memory implementation of TaskStore. + + Stores task objects in a dictionary in memory. Task data is lost when the + server process stops. + """ def __init__(self) -> None: + """Initializes the InMemoryTaskStore.""" logger.debug('Initializing InMemoryTaskStore') self.tasks: dict[str, Task] = {} self.lock = asyncio.Lock() async def save(self, task: Task) -> None: + """Saves or updates a task in the in-memory store.""" async with self.lock: self.tasks[task.id] = task logger.info('Task %s saved successfully.', task.id) async def get(self, task_id: str) -> Task | None: + """Retrieves a task from the in-memory store by ID.""" async with self.lock: logger.debug('Attempting to get task with id: %s', task_id) task = self.tasks.get(task_id) @@ -32,6 +39,7 @@ async def get(self, task_id: str) -> Task | None: return task async def delete(self, task_id: str) -> None: + """Deletes a task from the in-memory store by ID.""" async with self.lock: logger.debug('Attempting to delete task with id: %s', task_id) if task_id in self.tasks: diff --git a/src/a2a/server/tasks/push_notifier.py b/src/a2a/server/tasks/push_notifier.py index 10f01f3a..ca1246b8 100644 --- a/src/a2a/server/tasks/push_notifier.py +++ b/src/a2a/server/tasks/push_notifier.py @@ -10,16 +10,16 @@ class PushNotifier(ABC): async def set_info( self, task_id: str, notification_config: PushNotificationConfig ): - pass + """Sets or updates the push notification configuration for a task.""" @abstractmethod async def get_info(self, task_id: str) -> PushNotificationConfig | None: - pass + """Retrieves the push notification configuration for a task.""" @abstractmethod async def delete_info(self, task_id: str): - pass + """Deletes the push notification configuration for a task.""" @abstractmethod async def send_notification(self, task: Task): - pass + """Sends a push notification containing the latest task state.""" diff --git a/src/a2a/server/tasks/result_aggregator.py b/src/a2a/server/tasks/result_aggregator.py index 94f27403..a3a3326f 100644 --- a/src/a2a/server/tasks/result_aggregator.py +++ b/src/a2a/server/tasks/result_aggregator.py @@ -25,11 +25,25 @@ class ResultAggregator: """ def __init__(self, task_manager: TaskManager): + """Initializes the ResultAggregator. + + Args: + task_manager: The `TaskManager` instance to use for processing events + and managing the task state. + """ self.task_manager = task_manager self._message: Message | None = None @property async def current_result(self) -> Task | Message | None: + """Returns the current aggregated result (Task or Message). + + This is the latest state processed from the event stream. + + Returns: + The current `Task` object managed by the `TaskManager`, or the final + `Message` if one was received, or `None` if no result has been produced yet. + """ if self._message: return self._message return await self.task_manager.get_task() @@ -37,7 +51,18 @@ async def current_result(self) -> Task | Message | None: async def consume_and_emit( self, consumer: EventConsumer ) -> AsyncGenerator[Event]: - """Processes the event stream and emits the same event stream out.""" + """Processes the event stream from the consumer, updates the task state, and re-emits the same events. + + Useful for streaming scenarios where the server needs to observe and + process events (e.g., save task state, send push notifications) while + forwarding them to the client. + + Args: + consumer: The `EventConsumer` to read events from. + + Yields: + The `Event` objects consumed from the `EventConsumer`. + """ async for event in consumer.consume_all(): await self.task_manager.process(event) yield event @@ -45,7 +70,20 @@ async def consume_and_emit( async def consume_all( self, consumer: EventConsumer ) -> Task | Message | None: - """Processes the entire event stream and returns the final result.""" + """Processes the entire event stream from the consumer and returns the final result. + + Blocks until the event stream ends (queue is closed after final event or exception). + + Args: + consumer: The `EventConsumer` to read events from. + + Returns: + The final `Task` object or `Message` object after the stream is exhausted. + Returns `None` if the stream ends without producing a final result. + + Raises: + BaseException: If the `EventConsumer` raises an exception during consumption. + """ async for event in consumer.consume_all(): if isinstance(event, Message): self._message = event @@ -56,7 +94,23 @@ async def consume_all( async def consume_and_break_on_interrupt( self, consumer: EventConsumer ) -> tuple[Task | Message | None, bool]: - """Process the event stream until completion or an interruptable state is encountered.""" + """Processes the event stream until completion or an interruptable state is encountered. + + Interruptable states currently include `TaskState.auth_required`. + If interrupted, consumption continues in a background task. + + Args: + consumer: The `EventConsumer` to read events from. + + Returns: + A tuple containing: + - The current aggregated result (`Task` or `Message`) at the point of completion or interruption. + - A boolean indicating whether the consumption was interrupted (`True`) + or completed naturally (`False`). + + Raises: + BaseException: If the `EventConsumer` raises an exception during consumption. + """ event_stream = consumer.consume_all() interrupted = False async for event in event_stream: @@ -86,5 +140,13 @@ async def consume_and_break_on_interrupt( async def _continue_consuming( self, event_stream: AsyncIterator[Event] ) -> None: + """Continues processing an event stream in a background task. + + Used after an interruptable state (like auth_required) is encountered + in the synchronous consumption flow. + + Args: + event_stream: The remaining `AsyncIterator` of events from the consumer. + """ async for event in event_stream: await self.task_manager.process(event) diff --git a/src/a2a/server/tasks/task_manager.py b/src/a2a/server/tasks/task_manager.py index a9978bce..ca42b69b 100644 --- a/src/a2a/server/tasks/task_manager.py +++ b/src/a2a/server/tasks/task_manager.py @@ -19,7 +19,11 @@ class TaskManager: - """Helps manage a task's lifecycle during execution of a request.""" + """Helps manage a task's lifecycle during execution of a request. + + Responsible for retrieving, saving, and updating the `Task` object based on + events received from the agent. + """ def __init__( self, @@ -28,6 +32,15 @@ def __init__( task_store: TaskStore, initial_message: Message | None, ): + """Initializes the TaskManager. + + Args: + task_id: The ID of the task, if known from the request. + context_id: The ID of the context, if known from the request. + task_store: The `TaskStore` instance for persistence. + initial_message: The `Message` that initiated the task, if any. + Used when creating a new task object. + """ self.task_id = task_id self.context_id = context_id self.task_store = task_store @@ -40,6 +53,14 @@ def __init__( ) async def get_task(self) -> Task | None: + """Retrieves the current task object, either from memory or the store. + + If `task_id` is set, it first checks the in-memory `_current_task`, + then attempts to load it from the `task_store`. + + Returns: + The `Task` object if found, otherwise `None`. + """ if not self.task_id: logger.debug('task_id is not set, cannot get task.') return None @@ -60,6 +81,20 @@ async def get_task(self) -> Task | None: async def save_task_event( self, event: Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent ) -> Task | None: + """Processes a task-related event (Task, Status, Artifact) and saves the updated task state. + + Ensures task and context IDs match or are set from the event. + + Args: + event: The task-related event (`Task`, `TaskStatusUpdateEvent`, or `TaskArtifactUpdateEvent`). + + Returns: + The updated `Task` object after processing the event. + + Raises: + ServerError: If the task ID in the event conflicts with the TaskManager's ID + when the TaskManager's ID is already set. + """ task_id_from_event = ( event.id if isinstance(event, Task) else event.taskId ) @@ -107,6 +142,14 @@ async def save_task_event( async def ensure_task( self, event: TaskStatusUpdateEvent | TaskArtifactUpdateEvent ) -> Task: + """Ensures a Task object exists in memory, loading from store or creating new if needed. + + Args: + event: The task-related event triggering the need for a Task object. + + Returns: + An existing or newly created `Task` object. + """ task: Task | None = self._current_task if not task and self.task_id: logger.debug( @@ -128,9 +171,16 @@ async def ensure_task( return task async def process(self, event: Event) -> Event: - """Processes an event, store the task state and return the task or message. + """Processes an event, updates the task state if applicable, stores it, and returns the event. + + If the event is task-related (`Task`, `TaskStatusUpdateEvent`, `TaskArtifactUpdateEvent`), + the internal task state is updated and persisted. - The returned Task or Message represent the current status of the result. + Args: + event: The event object received from the agent. + + Returns: + The same event object that was processed. """ if isinstance( event, Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent @@ -140,7 +190,15 @@ async def process(self, event: Event) -> Event: return event def _init_task_obj(self, task_id: str, context_id: str) -> Task: - """Initializes a new task object.""" + """Initializes a new task object in memory. + + Args: + task_id: The ID for the new task. + context_id: The context ID for the new task. + + Returns: + A new `Task` object with initial status and potentially the initial message in history. + """ logger.debug( 'Initializing new Task object with task_id: %s, context_id: %s', task_id, @@ -155,6 +213,11 @@ def _init_task_obj(self, task_id: str, context_id: str) -> Task: ) async def _save_task(self, task: Task) -> None: + """Saves the given task to the task store and updates the in-memory `_current_task`. + + Args: + task: The `Task` object to save. + """ logger.debug('Saving task with id: %s', task.id) await self.task_store.save(task) self._current_task = task @@ -164,10 +227,17 @@ async def _save_task(self, task: Task) -> None: self.context_id = task.contextId def update_with_message(self, message: Message, task: Task) -> Task: - """Update the prior status to history, and add the incoming message to history. + """Updates a task object in memory by adding a new message to its history. + + If the task has a message in its current status, that message is moved + to the history first. + + Args: + message: The new `Message` to add to the history. + task: The `Task` object to update. - This updates the object in memory, and the _current_task, but does not - persist it. This is the job of the processor after this step. + Returns: + The updated `Task` object (updated in-place). """ if task.status.message: if task.history: diff --git a/src/a2a/server/tasks/task_store.py b/src/a2a/server/tasks/task_store.py index af856d6f..6d7ce59d 100644 --- a/src/a2a/server/tasks/task_store.py +++ b/src/a2a/server/tasks/task_store.py @@ -4,16 +4,19 @@ class TaskStore(ABC): - """Agent Task Store interface.""" + """Agent Task Store interface. + + Defines the methods for persisting and retrieving `Task` objects. + """ @abstractmethod async def save(self, task: Task): - pass + """Saves or updates a task in the store.""" @abstractmethod async def get(self, task_id: str) -> Task | None: - pass + """Retrieves a task from the store by ID.""" @abstractmethod async def delete(self, task_id: str): - pass + """Deletes a task from the store by ID.""" diff --git a/src/a2a/server/tasks/task_updater.py b/src/a2a/server/tasks/task_updater.py index fdecde5a..58c2ca13 100644 --- a/src/a2a/server/tasks/task_updater.py +++ b/src/a2a/server/tasks/task_updater.py @@ -16,9 +16,19 @@ class TaskUpdater: - """Helper class for publishing updates to a task.""" + """Helper class for agents to publish updates to a task's event queue. + + Simplifies the process of creating and enqueueing standard task events. + """ def __init__(self, event_queue: EventQueue, task_id: str, context_id: str): + """Initializes the TaskUpdater. + + Args: + event_queue: The `EventQueue` associated with the task. + task_id: The ID of the task. + context_id: The context ID of the task. + """ self.event_queue = event_queue self.task_id = task_id self.context_id = context_id @@ -26,7 +36,13 @@ def __init__(self, event_queue: EventQueue, task_id: str, context_id: str): def update_status( self, state: TaskState, message: Message | None = None, final=False ): - """Update the status of the task.""" + """Updates the status of the task and publishes a `TaskStatusUpdateEvent`. + + Args: + state: The new state of the task. + message: An optional message associated with the status update. + final: If True, indicates this is the final status update for the task. + """ self.event_queue.enqueue_event( TaskStatusUpdateEvent( taskId=self.task_id, @@ -42,11 +58,20 @@ def update_status( def add_artifact( self, parts: list[Part], - artifact_id=str(uuid.uuid4()), + artifact_id: str = str(uuid.uuid4()), name: str | None = None, metadata: dict[str, Any] | None = None, ): - """Add an artifact to the task.""" + """Adds an artifact chunk to the task and publishes a `TaskArtifactUpdateEvent`. + + Args: + parts: A list of `Part` objects forming the artifact chunk. + artifact_id: The ID of the artifact. A new UUID is generated if not provided. + name: Optional name for the artifact. + metadata: Optional metadata for the artifact. + append: Optional boolean indicating if this chunk appends to a previous one. + last_chunk: Optional boolean indicating if this is the last chunk. + """ self.event_queue.enqueue_event( TaskArtifactUpdateEvent( taskId=self.task_id, @@ -61,7 +86,7 @@ def add_artifact( ) def complete(self, message: Message | None = None): - """Mark the task as completed.""" + """Marks the task as completed and publishes a final status update.""" self.update_status( TaskState.completed, message=message, @@ -69,27 +94,42 @@ def complete(self, message: Message | None = None): ) def failed(self, message: Message | None = None): - """Mark the task as failed.""" + """Marks the task as failed and publishes a final status update.""" self.update_status(TaskState.failed, message=message, final=True) def submit(self, message: Message | None = None): - """Mark the task as submitted.""" + """Marks the task as submitted and publishes a status update.""" self.update_status( TaskState.submitted, message=message, ) def start_work(self, message: Message | None = None): - """Mark the task as working.""" + """Marks the task as working and publishes a status update.""" self.update_status( TaskState.working, message=message, ) def new_agent_message( - self, parts: list[Part], final=False, metadata=None + self, + parts: list[Part], + final: bool | None = None, + metadata: dict[str, Any] | None = None, ) -> Message: - """Create a new message for the task.""" + """Creates a new message object sent by the agent for this task/context. + + Note: This method only *creates* the message object. It does not + automatically enqueue it. + + Args: + parts: A list of `Part` objects for the message content. + final: Optional boolean indicating if this is the final message in a stream. + metadata: Optional metadata for the message. + + Returns: + A new `Message` object. + """ return Message( role=Role.agent, taskId=self.task_id, diff --git a/src/a2a/types.py b/src/a2a/types.py index a7d1155a..e04c5973 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -1,6 +1,8 @@ # generated by datamodel-codegen: # filename: spec.json +"""Data models representing the A2A protocol.""" + from __future__ import annotations from enum import Enum @@ -10,13 +12,13 @@ class A2A(RootModel[Any]): + """Root model for the A2A specification.""" + root: Any class In(Enum): - """ - The location of the API key. Valid values are "query", "header", or "cookie". - """ + """The location of the API key. Valid values are "query", "header", or "cookie".""" cookie = 'cookie' header = 'header' @@ -24,713 +26,377 @@ class In(Enum): class APIKeySecurityScheme(BaseModel): - """ - API Key security scheme. - """ + """API Key security scheme.""" description: str | None = None - """ - description of this security scheme - """ + """Description of this security scheme.""" in_: In = Field(..., alias='in') - """ - The location of the API key. Valid values are "query", "header", or "cookie". - """ + """The location of the API key. Valid values are "query", "header", or "cookie".""" name: str - """ - The name of the header, query or cookie parameter to be used. - """ + """The name of the header, query or cookie parameter to be used.""" type: Literal['apiKey'] = 'apiKey' class AgentCapabilities(BaseModel): - """ - Defines optional capabilities supported by an agent. - """ + """Defines optional capabilities supported by an agent.""" pushNotifications: bool | None = None - """ - true if the agent can notify updates to client - """ + """True if the agent can notify updates to client.""" stateTransitionHistory: bool | None = None - """ - true if the agent exposes status change history for tasks - """ + """True if the agent exposes status change history for tasks.""" streaming: bool | None = None - """ - true if the agent supports SSE - """ + """True if the agent supports SSE.""" class AgentProvider(BaseModel): - """ - Represents the service provider of an agent. - """ + """Represents the service provider of an agent.""" organization: str - """ - Agent provider's organization name - """ + """Agent provider's organization name.""" url: str - """ - Agent provider's url - """ + """Agent provider's url.""" class AgentSkill(BaseModel): - """ - Represents a unit of capability that an agent can perform. - """ + """Represents a unit of capability that an agent can perform.""" description: str - """ - description of the skill - will be used by the client or a human - as a hint to understand what the skill does. - """ + """Description of the skill - will be used by the client or a human as a hint to understand what the skill does.""" examples: list[str] | None = None - """ - The set of example scenarios that the skill can perform. - Will be used by the client as a hint to understand how the skill can be - used. - """ + """The set of example scenarios that the skill can perform. Will be used by the client as a hint to understand how the skill can be used.""" id: str - """ - unique identifier for the agent's skill - """ + """Unique identifier for the agent's skill.""" inputModes: list[str] | None = None - """ - The set of interaction modes that the skill supports - (if different than the default). - Supported mime types for input. - """ + """The set of interaction modes that the skill supports (if different than the default). Supported mime types for input.""" name: str - """ - human readable name of the skill - """ + """Human readable name of the skill.""" outputModes: list[str] | None = None - """ - Supported mime types for output. - """ + """Supported mime types for output.""" tags: list[str] - """ - Set of tagwords describing classes of capabilities for this specific - skill. - """ + """Set of tagwords describing classes of capabilities for this specific skill.""" class AuthorizationCodeOAuthFlow(BaseModel): - """ - Configuration details for a supported OAuth Flow - """ + """Configuration details for a supported OAuth Flow.""" authorizationUrl: str - """ - The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS - """ + """The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" refreshUrl: str | None = None - """ - The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS. - """ + """The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" scopes: dict[str, str] - """ - The available scopes for the OAuth2 security scheme. A map between the scope name and a short - description for it. The map MAY be empty. - """ + """The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.""" tokenUrl: str - """ - The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - requires the use of TLS. - """ + """The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" class ClientCredentialsOAuthFlow(BaseModel): - """ - Configuration details for a supported OAuth Flow - """ + """Configuration details for a supported OAuth Flow.""" refreshUrl: str | None = None - """ - The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS. - """ + """The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" scopes: dict[str, str] - """ - The available scopes for the OAuth2 security scheme. A map between the scope name and a short - description for it. The map MAY be empty. - """ + """The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.""" tokenUrl: str - """ - The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - requires the use of TLS. - """ + """The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" class ContentTypeNotSupportedError(BaseModel): - """ - A2A specific error indicating incompatible content types between request and agent capabilities. - """ + """A2A specific error indicating incompatible content types between request and agent capabilities.""" code: Literal[-32005] = -32005 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Incompatible content types' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class DataPart(BaseModel): - """ - Represents a structured data segment within a message part. - """ + """Represents a structured data segment within a message part.""" data: dict[str, Any] - """ - Structured data content - """ + """Structured data content.""" kind: Literal['data'] = 'data' - """ - Part type - data for DataParts - """ + """Part type - data for DataParts.""" metadata: dict[str, Any] | None = None - """ - Optional metadata associated with the part. - """ + """Optional metadata associated with the part.""" class FileBase(BaseModel): - """ - Represents the base entity for FileParts - """ + """Represents the base entity for FileParts.""" mimeType: str | None = None - """ - Optional mimeType for the file - """ + """Optional mimeType for the file.""" name: str | None = None - """ - Optional name for the file - """ + """Optional name for the file.""" -class FileWithBytes(BaseModel): - """ - Define the variant where 'bytes' is present and 'uri' is absent - """ +class FileWithBytes(FileBase): + """Define the variant where 'bytes' is present and 'uri' is absent.""" bytes: str - """ - base64 encoded content of the file - """ - mimeType: str | None = None - """ - Optional mimeType for the file - """ - name: str | None = None - """ - Optional name for the file - """ + """Base64 encoded content of the file.""" -class FileWithUri(BaseModel): - """ - Define the variant where 'uri' is present and 'bytes' is absent - """ +class FileWithUri(FileBase): + """Define the variant where 'uri' is present and 'bytes' is absent.""" - mimeType: str | None = None - """ - Optional mimeType for the file - """ - name: str | None = None - """ - Optional name for the file - """ uri: str class HTTPAuthSecurityScheme(BaseModel): - """ - HTTP Authentication security scheme. - """ + """HTTP Authentication security scheme.""" bearerFormat: str | None = None - """ - A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually - generated by an authorization server, so this information is primarily for documentation - purposes. - """ + """A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes.""" description: str | None = None - """ - description of this security scheme - """ + """Description of this security scheme.""" scheme: str - """ - The name of the HTTP Authentication scheme to be used in the Authorization header as defined - in RFC7235. The values used SHOULD be registered in the IANA Authentication Scheme registry. - The value is case-insensitive, as defined in RFC7235. - """ + """The name of the HTTP Authentication scheme to be used in the Authorization header as defined in RFC7235. The values used SHOULD be registered in the IANA Authentication Scheme registry. The value is case-insensitive, as defined in RFC7235.""" type: Literal['http'] = 'http' class ImplicitOAuthFlow(BaseModel): - """ - Configuration details for a supported OAuth Flow - """ + """Configuration details for a supported OAuth Flow.""" authorizationUrl: str - """ - The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS - """ + """The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" refreshUrl: str | None = None - """ - The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS. - """ + """The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" scopes: dict[str, str] - """ - The available scopes for the OAuth2 security scheme. A map between the scope name and a short - description for it. The map MAY be empty. - """ + """The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.""" class InternalError(BaseModel): - """ - JSON-RPC error indicating an internal JSON-RPC error on the server. - """ + """JSON-RPC error indicating an internal JSON-RPC error on the server.""" code: Literal[-32603] = -32603 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Internal error' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class InvalidAgentResponseError(BaseModel): - """ - A2A specific error indicating agent returned invalid response for the current method - """ + """A2A specific error indicating agent returned invalid response for the current method.""" code: Literal[-32006] = -32006 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Invalid agent response' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class InvalidParamsError(BaseModel): - """ - JSON-RPC error indicating invalid method parameter(s). - """ + """JSON-RPC error indicating invalid method parameter(s).""" code: Literal[-32602] = -32602 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Invalid parameters' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class InvalidRequestError(BaseModel): - """ - JSON-RPC error indicating the JSON sent is not a valid Request object. - """ + """JSON-RPC error indicating the JSON sent is not a valid Request object.""" code: Literal[-32600] = -32600 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Request payload validation error' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class JSONParseError(BaseModel): - """ - JSON-RPC error indicating invalid JSON was received by the server. - """ + """JSON-RPC error indicating invalid JSON was received by the server.""" code: Literal[-32700] = -32700 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Invalid JSON payload' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class JSONRPCError(BaseModel): - """ - Represents a JSON-RPC 2.0 Error object. - This is typically included in a JSONRPCErrorResponse when an error occurs. - """ + """Represents a JSON-RPC 2.0 Error object.""" code: int - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class JSONRPCMessage(BaseModel): - """ - Base interface for any JSON-RPC 2.0 request or response. - """ + """Base interface for any JSON-RPC 2.0 request or response.""" id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ + """An identifier established by the Client that MUST contain a String, Number. Numbers SHOULD NOT contain fractional parts.""" jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ + """Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0".""" -class JSONRPCRequest(BaseModel): - """ - Represents a JSON-RPC 2.0 Request object. - """ +class JSONRPCRequest(JSONRPCMessage): + """Represents a JSON-RPC 2.0 Request object.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: str - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: dict[str, Any] | None = None - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class JSONRPCResult(BaseModel): - """ - Represents a JSON-RPC 2.0 Result object. - """ +class JSONRPCResult(JSONRPCMessage): + """Represents a JSON-RPC 2.0 Result object.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: Any - """ - The result object on success - """ + """The result object on success.""" class Role(Enum): - """ - message sender's role - """ + """Message sender's role.""" agent = 'agent' user = 'user' class MethodNotFoundError(BaseModel): - """ - JSON-RPC error indicating the method does not exist or is not available. - """ + """JSON-RPC error indicating the method does not exist or is not available.""" code: Literal[-32601] = -32601 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Method not found' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class OpenIdConnectSecurityScheme(BaseModel): - """ - OpenID Connect security scheme configuration. - """ + """OpenID Connect security scheme configuration.""" description: str | None = None - """ - description of this security scheme - """ + """Description of this security scheme.""" openIdConnectUrl: str - """ - Well-known URL to discover the [[OpenID-Connect-Discovery]] provider metadata. - """ + """Well-known URL to discover the [[OpenID-Connect-Discovery]] provider metadata.""" type: Literal['openIdConnect'] = 'openIdConnect' class PartBase(BaseModel): - """ - Base properties common to all message parts. - """ + """Base properties common to all message parts.""" metadata: dict[str, Any] | None = None - """ - Optional metadata associated with the part. - """ + """Optional metadata associated with the part.""" class PasswordOAuthFlow(BaseModel): - """ - Configuration details for a supported OAuth Flow - """ + """Configuration details for a supported OAuth Flow.""" refreshUrl: str | None = None - """ - The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - standard requires the use of TLS. - """ + """The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" scopes: dict[str, str] - """ - The available scopes for the OAuth2 security scheme. A map between the scope name and a short - description for it. The map MAY be empty. - """ + """The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.""" tokenUrl: str - """ - The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - requires the use of TLS. - """ + """The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.""" class PushNotificationAuthenticationInfo(BaseModel): - """ - Defines authentication details for push notifications. - """ + """Defines authentication details for push notifications.""" credentials: str | None = None - """ - Optional credentials - """ + """Optional credentials.""" schemes: list[str] - """ - Supported authentication schemes - e.g. Basic, Bearer - """ + """Supported authentication schemes - e.g. Basic, Bearer.""" class PushNotificationConfig(BaseModel): - """ - Configuration for setting up push notifications for task updates. - """ + """Configuration for setting up push notifications for task updates.""" authentication: PushNotificationAuthenticationInfo | None = None token: str | None = None - """ - token unique to this task/session - """ + """Token unique to this task/session.""" url: str - """ - url for sending the push notifications - """ + """URL for sending the push notifications.""" class PushNotificationNotSupportedError(BaseModel): - """ - A2A specific error indicating the agent does not support push notifications. - """ + """A2A specific error indicating the agent does not support push notifications.""" code: Literal[-32003] = -32003 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Push Notification is not supported' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class SecuritySchemeBase(BaseModel): - """ - Base properties shared by all security schemes. - """ + """Base properties shared by all security schemes.""" description: str | None = None - """ - description of this security scheme - """ + """Description of this security scheme.""" class TaskIdParams(BaseModel): - """ - Parameters containing only a task ID, used for simple task operations. - """ + """Parameters containing only a task ID, used for simple task operations.""" id: str - """ - task id - """ + """Task id.""" metadata: dict[str, Any] | None = None class TaskNotCancelableError(BaseModel): - """ - A2A specific error indicating the task is in a state where it cannot be canceled. - """ + """A2A specific error indicating the task is in a state where it cannot be canceled.""" code: Literal[-32002] = -32002 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Task cannot be canceled' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class TaskNotFoundError(BaseModel): - """ - A2A specific error indicating the requested task ID was not found. - """ + """A2A specific error indicating the requested task ID was not found.""" code: Literal[-32001] = -32001 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'Task not found' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class TaskPushNotificationConfig(BaseModel): - """ - Parameters for setting or getting push notification configuration for a task - """ + """Parameters for setting or getting push notification configuration for a task.""" pushNotificationConfig: PushNotificationConfig taskId: str - """ - task id - """ + """Task id.""" -class TaskQueryParams(BaseModel): - """ - Parameters for querying a task, including optional history length. - """ +class TaskQueryParams(TaskIdParams): + """Parameters for querying a task, including optional history length.""" historyLength: int | None = None - """ - number of recent messages to be retrieved - """ - id: str - """ - task id - """ - metadata: dict[str, Any] | None = None + """Number of recent messages to be retrieved.""" -class TaskResubscriptionRequest(BaseModel): - """ - JSON-RPC request model for the 'tasks/resubscribe' method. - """ +class TaskResubscriptionRequest(JSONRPCRequest): + """JSON-RPC request model for the 'tasks/resubscribe' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['tasks/resubscribe'] = 'tasks/resubscribe' - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: TaskIdParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" class TaskState(Enum): - """ - Represents the possible states of a Task. - """ + """Represents the possible states of a Task.""" submitted = 'submitted' working = 'working' @@ -743,43 +409,24 @@ class TaskState(Enum): unknown = 'unknown' -class TextPart(BaseModel): - """ - Represents a text segment within parts. - """ +class TextPart(PartBase): + """Represents a text segment within parts.""" kind: Literal['text'] = 'text' - """ - Part type - text for TextParts - """ - metadata: dict[str, Any] | None = None - """ - Optional metadata associated with the part. - """ + """Part type - text for TextParts.""" text: str - """ - Text content - """ + """Text content.""" class UnsupportedOperationError(BaseModel): - """ - A2A specific error indicating the requested operation is not supported by the agent. - """ + """A2A specific error indicating the requested operation is not supported by the agent.""" code: Literal[-32004] = -32004 - """ - A Number that indicates the error type that occurred. - """ + """A Number that indicates the error type that occurred.""" data: Any | None = None - """ - A Primitive or Structured value that contains additional information about the error. - This may be omitted. - """ + """A Primitive or Structured value that contains additional information about the error. This may be omitted.""" message: str | None = 'This operation is not supported' - """ - A String providing a short description of the error. - """ + """A String providing a short description of the error.""" class A2AError( @@ -797,6 +444,8 @@ class A2AError( | InvalidAgentResponseError ] ): + """Represents A2A specific JSON-RPC error responses.""" + root: ( JSONParseError | InvalidRequestError @@ -812,123 +461,53 @@ class A2AError( ) -class CancelTaskRequest(BaseModel): - """ - JSON-RPC request model for the 'tasks/cancel' method. - """ +class CancelTaskRequest(JSONRPCRequest): + """JSON-RPC request model for the 'tasks/cancel' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['tasks/cancel'] = 'tasks/cancel' - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: TaskIdParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class FilePart(BaseModel): - """ - Represents a File segment within parts. - """ +class FilePart(PartBase): + """Represents a File segment within parts.""" file: FileWithBytes | FileWithUri - """ - File content either as url or bytes - """ + """File content either as url or bytes.""" kind: Literal['file'] = 'file' - """ - Part type - file for FileParts - """ - metadata: dict[str, Any] | None = None - """ - Optional metadata associated with the part. - """ + """Part type - file for FileParts.""" -class GetTaskPushNotificationConfigRequest(BaseModel): - """ - JSON-RPC request model for the 'tasks/pushNotificationConfig/get' method. - """ +class GetTaskPushNotificationConfigRequest(JSONRPCRequest): + """JSON-RPC request model for the 'tasks/pushNotificationConfig/get' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['tasks/pushNotificationConfig/get'] = ( 'tasks/pushNotificationConfig/get' ) - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: TaskIdParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class GetTaskPushNotificationConfigSuccessResponse(BaseModel): - """ - JSON-RPC success response model for the 'tasks/pushNotificationConfig/get' method. - """ +class GetTaskPushNotificationConfigSuccessResponse(JSONRPCResult): + """JSON-RPC success response model for the 'tasks/pushNotificationConfig/get' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: TaskPushNotificationConfig - """ - The result object on success - """ + """The result object on success.""" -class GetTaskRequest(BaseModel): - """ - JSON-RPC request model for the 'tasks/get' method. - """ +class GetTaskRequest(JSONRPCRequest): + """JSON-RPC request model for the 'tasks/get' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['tasks/get'] = 'tasks/get' - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: TaskQueryParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class JSONRPCErrorResponse(BaseModel): - """ - Represents a JSON-RPC 2.0 Error Response object. - """ +class JSONRPCErrorResponse(JSONRPCMessage): + """Represents a JSON-RPC 2.0 Error Response object.""" error: ( JSONRPCError @@ -941,144 +520,74 @@ class JSONRPCErrorResponse(BaseModel): | TaskNotCancelableError | PushNotificationNotSupportedError | UnsupportedOperationError - | ContentTypeNotSupportedError - | InvalidAgentResponseError - ) - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ + | ContentTypeNotSupportedError + | InvalidAgentResponseError + ) class MessageSendConfiguration(BaseModel): - """ - Configuration for the send message request - """ + """Configuration for the send message request.""" acceptedOutputModes: list[str] - """ - accepted output modalities by the client - """ + """Accepted output modalities by the client.""" blocking: bool | None = None - """ - If the server should treat the client as a blocking request - """ + """If the server should treat the client as a blocking request.""" historyLength: int | None = None - """ - number of recent messages to be retrieved - """ + """Number of recent messages to be retrieved.""" pushNotificationConfig: PushNotificationConfig | None = None - """ - where the server should send notifications when disconnected. - """ + """Where the server should send notifications when disconnected.""" class OAuthFlows(BaseModel): - """ - Allows configuration of the supported OAuth Flows - """ + """Allows configuration of the supported OAuth Flows.""" authorizationCode: AuthorizationCodeOAuthFlow | None = None - """ - Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. - """ + """Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0.""" clientCredentials: ClientCredentialsOAuthFlow | None = None - """ - Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0 - """ + """Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0.""" implicit: ImplicitOAuthFlow | None = None - """ - Configuration for the OAuth Implicit flow - """ + """Configuration for the OAuth Implicit flow.""" password: PasswordOAuthFlow | None = None - """ - Configuration for the OAuth Resource Owner Password flow - """ + """Configuration for the OAuth Resource Owner Password flow.""" class Part(RootModel[TextPart | FilePart | DataPart]): + """Represents a part of a message, which can be text, a file, or structured data.""" + root: TextPart | FilePart | DataPart - """ - Represents a part of a message, which can be text, a file, or structured data. - """ -class SetTaskPushNotificationConfigRequest(BaseModel): - """ - JSON-RPC request model for the 'tasks/pushNotificationConfig/set' method. - """ +class SetTaskPushNotificationConfigRequest(JSONRPCRequest): + """JSON-RPC request model for the 'tasks/pushNotificationConfig/set' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['tasks/pushNotificationConfig/set'] = ( 'tasks/pushNotificationConfig/set' ) - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: TaskPushNotificationConfig - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class SetTaskPushNotificationConfigSuccessResponse(BaseModel): - """ - JSON-RPC success response model for the 'tasks/pushNotificationConfig/set' method. - """ +class SetTaskPushNotificationConfigSuccessResponse(JSONRPCResult): + """JSON-RPC success response model for the 'tasks/pushNotificationConfig/set' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: TaskPushNotificationConfig - """ - The result object on success - """ + """The result object on success.""" class Artifact(BaseModel): - """ - Represents an artifact generated for a task task. - """ + """Represents an artifact generated for a task task.""" artifactId: str - """ - unique identifier for the artifact - """ + """Unique identifier for the artifact.""" description: str | None = None - """ - Optional description for the artifact - """ + """Optional description for the artifact.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" name: str | None = None - """ - Optional name for the artifact - """ + """Optional name for the artifact.""" parts: list[Part] - """ - artifact parts - """ + """Artifact parts.""" class GetTaskPushNotificationConfigResponse( @@ -1086,79 +595,50 @@ class GetTaskPushNotificationConfigResponse( JSONRPCErrorResponse | GetTaskPushNotificationConfigSuccessResponse ] ): + """JSON-RPC response for the 'tasks/pushNotificationConfig/get' method.""" + root: JSONRPCErrorResponse | GetTaskPushNotificationConfigSuccessResponse - """ - JSON-RPC response for the 'tasks/pushNotificationConfig/set' method. - """ class Message(BaseModel): - """ - Represents a single message exchanged between user and agent. - """ + """Represents a single message exchanged between user and agent.""" contextId: str | None = None - """ - the context the message is associated with - """ + """The context the message is associated with.""" kind: Literal['message'] = 'message' - """ - event type - """ + """Event type.""" messageId: str - """ - identifier created by the message creator - """ + """Identifier created by the message creator.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" parts: list[Part] - """ - message content - """ + """Message content.""" role: Role - """ - message sender's role - """ + """Message sender's role.""" taskId: str | None = None - """ - identifier of task the message is related to - """ + """Identifier of task the message is related to.""" + final: bool | None = None + """Indicates if this is the final message in a stream.""" class MessageSendParams(BaseModel): - """ - Sent by the client to the agent as a request. May create, continue or restart a task. - """ + """Sent by the client to the agent as a request. May create, continue or restart a task.""" configuration: MessageSendConfiguration | None = None - """ - Send message configuration - """ + """Send message configuration.""" message: Message - """ - The message being sent to the server - """ + """The message being sent to the server.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" class OAuth2SecurityScheme(BaseModel): - """ - OAuth2.0 security scheme configuration. - """ + """OAuth2.0 security scheme configuration.""" description: str | None = None - """ - description of this security scheme - """ + """Description of this security scheme.""" flows: OAuthFlows - """ - An object containing configuration information for the flow types supported. - """ + """An object containing configuration information for the flow types supported.""" type: Literal['oauth2'] = 'oauth2' @@ -1170,64 +650,32 @@ class SecurityScheme( | OpenIdConnectSecurityScheme ] ): + """Mirrors the OpenAPI Security Scheme Object (https://swagger.io/specification/#security-scheme-object).""" + root: ( APIKeySecurityScheme | HTTPAuthSecurityScheme | OAuth2SecurityScheme | OpenIdConnectSecurityScheme ) - """ - Mirrors the OpenAPI Security Scheme Object - (https://swagger.io/specification/#security-scheme-object) - """ -class SendMessageRequest(BaseModel): - """ - JSON-RPC request model for the 'message/send' method. - """ +class SendMessageRequest(JSONRPCRequest): + """JSON-RPC request model for the 'message/send' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['message/send'] = 'message/send' - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: MessageSendParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" -class SendStreamingMessageRequest(BaseModel): - """ - JSON-RPC request model for the 'message/stream' method. - """ +class SendStreamingMessageRequest(JSONRPCRequest): + """JSON-RPC request model for the 'message/stream' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ method: Literal['message/stream'] = 'message/stream' - """ - A String containing the name of the method to be invoked. - """ + """A String containing the name of the method to be invoked.""" params: MessageSendParams - """ - A Structured value that holds the parameter values to be used during the invocation of the method. - """ + """A Structured value that holds the parameter values to be used during the invocation of the method.""" class SetTaskPushNotificationConfigResponse( @@ -1235,92 +683,55 @@ class SetTaskPushNotificationConfigResponse( JSONRPCErrorResponse | SetTaskPushNotificationConfigSuccessResponse ] ): + """JSON-RPC response for the 'tasks/pushNotificationConfig/set' method.""" + root: JSONRPCErrorResponse | SetTaskPushNotificationConfigSuccessResponse - """ - JSON-RPC response for the 'tasks/pushNotificationConfig/set' method. - """ class TaskArtifactUpdateEvent(BaseModel): - """ - sent by server during sendStream or subscribe requests - """ + """Event sent by server during sendStream or subscribe requests indicating an artifact update.""" append: bool | None = None - """ - Indicates if this artifact appends to a previous one - """ + """Indicates if this artifact appends to a previous one.""" artifact: Artifact - """ - generated artifact - """ + """Generated artifact.""" contextId: str - """ - the context the task is associated with - """ + """The context the task is associated with.""" kind: Literal['artifact-update'] = 'artifact-update' - """ - event type - """ + """Event type.""" lastChunk: bool | None = None - """ - Indicates if this is the last chunk of the artifact - """ + """Indicates if this is the last chunk of the artifact.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" taskId: str - """ - Task id - """ + """Task id.""" class TaskStatus(BaseModel): - """ - TaskState and accompanying message. - """ + """TaskState and accompanying message.""" message: Message | None = None - """ - additional status updates for client - """ + """Additional status updates for client.""" state: TaskState timestamp: str | None = None - """ - ISO 8601 datetime string when the status was recorded. - """ + """ISO 8601 datetime string when the status was recorded.""" class TaskStatusUpdateEvent(BaseModel): - """ - sent by server during sendStream or subscribe requests - """ + """Event sent by server during sendStream or subscribe requests indicating a status update.""" contextId: str - """ - the context the task is associated with - """ + """The context the task is associated with.""" final: bool - """ - indicates the end of the event stream - """ + """Indicates the end of the event stream for this task (implies the task is finished or failed).""" kind: Literal['status-update'] = 'status-update' - """ - event type - """ + """Event type.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" status: TaskStatus - """ - current status of the task - """ + """Current status of the task.""" taskId: str - """ - Task id - """ + """Task id.""" class A2ARequest( @@ -1334,6 +745,8 @@ class A2ARequest( | TaskResubscriptionRequest ] ): + """A2A supported request types.""" + root: ( SendMessageRequest | SendStreamingMessageRequest @@ -1343,14 +756,12 @@ class A2ARequest( | GetTaskPushNotificationConfigRequest | TaskResubscriptionRequest ) - """ - A2A supported request types - """ class AgentCard(BaseModel): - """ - An AgentCard conveys key information: + """An AgentCard conveys key information about an agent. + + Includes: - Overall details (version, name, description, uses) - Skills: A set of capabilities the agent can perform - Default modalities/content types supported by the agent. @@ -1358,180 +769,90 @@ class AgentCard(BaseModel): """ capabilities: AgentCapabilities - """ - Optional capabilities supported by the agent. - """ + """Optional capabilities supported by the agent.""" defaultInputModes: list[str] - """ - The set of interaction modes that the agent - supports across all skills. This can be overridden per-skill. - Supported mime types for input. - """ + """The set of interaction modes that the agent supports across all skills. This can be overridden per-skill. Supported mime types for input.""" defaultOutputModes: list[str] - """ - Supported mime types for output. - """ + """Supported mime types for output.""" description: str - """ - A human-readable description of the agent. Used to assist users and - other agents in understanding what the agent can do. - """ + """A human-readable description of the agent. Used to assist users and other agents in understanding what the agent can do.""" documentationUrl: str | None = None - """ - A URL to documentation for the agent. - """ + """A URL to documentation for the agent.""" name: str - """ - Human readable name of the agent. - """ + """Human readable name of the agent.""" provider: AgentProvider | None = None - """ - The service provider of the agent - """ + """The service provider of the agent.""" security: list[dict[str, list[str]]] | None = None - """ - Security requirements for contacting the agent. - """ + """Security requirements for contacting the agent.""" securitySchemes: dict[str, SecurityScheme] | None = None - """ - Security scheme details used for authenticating with this agent. - """ + """Security scheme details used for authenticating with this agent.""" skills: list[AgentSkill] - """ - Skills are a unit of capability that an agent can perform. - """ + """Skills are a unit of capability that an agent can perform.""" url: str - """ - A URL to the address the agent is hosted at. - """ + """A URL to the address the agent is hosted at.""" version: str - """ - The version of the agent - format is up to the provider. - """ + """The version of the agent - format is up to the provider.""" class Task(BaseModel): + """Represents a task managed by the agent.""" + artifacts: list[Artifact] | None = None - """ - collection of artifacts created by the agent. - """ + """Collection of artifacts created by the agent.""" contextId: str - """ - server-generated id for contextual alignment across interactions - """ + """Server-generated id for contextual alignment across interactions.""" history: list[Message] | None = None + """History of messages associated with the task.""" id: str - """ - unique identifier for the task - """ + """Unique identifier for the task.""" kind: Literal['task'] = 'task' - """ - event type - """ + """Event type.""" metadata: dict[str, Any] | None = None - """ - extension metadata - """ + """Extension metadata.""" status: TaskStatus - """ - current status of the task - """ + """Current status of the task.""" -class CancelTaskSuccessResponse(BaseModel): - """ - JSON-RPC success response model for the 'tasks/cancel' method. - """ +class CancelTaskSuccessResponse(JSONRPCResult): + """JSON-RPC success response model for the 'tasks/cancel' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: Task - """ - The result object on success - """ + """The result object on success.""" -class GetTaskSuccessResponse(BaseModel): - """ - JSON-RPC success response for the 'tasks/get' method. - """ +class GetTaskSuccessResponse(JSONRPCResult): + """JSON-RPC success response for the 'tasks/get' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: Task - """ - The result object on success - """ + """The result object on success.""" -class SendMessageSuccessResponse(BaseModel): - """ - JSON-RPC success response model for the 'message/send' method. - """ +class SendMessageSuccessResponse(JSONRPCResult): + """JSON-RPC success response model for the 'message/send' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: Task | Message - """ - The result object on success - """ + """The result object on success.""" -class SendStreamingMessageSuccessResponse(BaseModel): - """ - JSON-RPC success response model for the 'message/stream' method. - """ +class SendStreamingMessageSuccessResponse(JSONRPCResult): + """JSON-RPC success response model for the 'message/stream' method.""" - id: str | int | None = None - """ - An identifier established by the Client that MUST contain a String, Number - Numbers SHOULD NOT contain fractional parts. - """ - jsonrpc: Literal['2.0'] = '2.0' - """ - Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". - """ result: Task | Message | TaskStatusUpdateEvent | TaskArtifactUpdateEvent - """ - The result object on success - """ + """The result object on success.""" class CancelTaskResponse( RootModel[JSONRPCErrorResponse | CancelTaskSuccessResponse] ): + """JSON-RPC response for the 'tasks/cancel' method.""" + root: JSONRPCErrorResponse | CancelTaskSuccessResponse - """ - JSON-RPC response for the 'tasks/cancel' method. - """ class GetTaskResponse(RootModel[JSONRPCErrorResponse | GetTaskSuccessResponse]): + """JSON-RPC success response for the 'tasks/get' method.""" + root: JSONRPCErrorResponse | GetTaskSuccessResponse - """ - JSON-RPC success response for the 'tasks/get' method. - """ class JSONRPCResponse( @@ -1545,6 +866,8 @@ class JSONRPCResponse( | GetTaskPushNotificationConfigSuccessResponse ] ): + """Represents a JSON-RPC 2.0 Response object.""" + root: ( JSONRPCErrorResponse | SendMessageSuccessResponse @@ -1554,24 +877,19 @@ class JSONRPCResponse( | SetTaskPushNotificationConfigSuccessResponse | GetTaskPushNotificationConfigSuccessResponse ) - """ - Represents a JSON-RPC 2.0 Response object. - """ class SendMessageResponse( RootModel[JSONRPCErrorResponse | SendMessageSuccessResponse] ): + """JSON-RPC response model for the 'message/send' method.""" + root: JSONRPCErrorResponse | SendMessageSuccessResponse - """ - JSON-RPC response model for the 'message/send' method. - """ class SendStreamingMessageResponse( RootModel[JSONRPCErrorResponse | SendStreamingMessageSuccessResponse] ): + """JSON-RPC response model for the 'message/stream' method.""" + root: JSONRPCErrorResponse | SendStreamingMessageSuccessResponse - """ - JSON-RPC response model for the 'message/stream' method. - """ diff --git a/src/a2a/utils/__init__.py b/src/a2a/utils/__init__.py index 840765c3..eac4ee17 100644 --- a/src/a2a/utils/__init__.py +++ b/src/a2a/utils/__init__.py @@ -1,3 +1,5 @@ +"""Utility functions for the A2A Python SDK.""" + from a2a.utils.artifact import ( new_artifact, new_data_artifact, diff --git a/src/a2a/utils/artifact.py b/src/a2a/utils/artifact.py index bcf92286..ee91a891 100644 --- a/src/a2a/utils/artifact.py +++ b/src/a2a/utils/artifact.py @@ -1,3 +1,5 @@ +"""Utility functions for creating A2A Artifact objects.""" + import uuid from typing import Any @@ -8,6 +10,16 @@ def new_artifact( parts: list[Part], name: str, description: str = '' ) -> Artifact: + """Creates a new Artifact object. + + Args: + parts: The list of `Part` objects forming the artifact's content. + name: The human-readable name of the artifact. + description: An optional description of the artifact. + + Returns: + A new `Artifact` object with a generated artifactId. + """ return Artifact( artifactId=str(uuid.uuid4()), parts=parts, @@ -21,6 +33,16 @@ def new_text_artifact( text: str, description: str = '', ) -> Artifact: + """Creates a new Artifact object containing only a single TextPart. + + Args: + name: The human-readable name of the artifact. + text: The text content of the artifact. + description: An optional description of the artifact. + + Returns: + A new `Artifact` object with a generated artifactId. + """ return new_artifact( [Part(root=TextPart(text=text))], name, @@ -32,7 +54,17 @@ def new_data_artifact( name: str, data: dict[str, Any], description: str = '', -): +) -> Artifact: + """Creates a new Artifact object containing only a single DataPart. + + Args: + name: The human-readable name of the artifact. + data: The structured data content of the artifact. + description: An optional description of the artifact. + + Returns: + A new `Artifact` object with a generated artifactId. + """ return new_artifact( [Part(root=DataPart(data=data))], name, diff --git a/src/a2a/utils/errors.py b/src/a2a/utils/errors.py index 49cc7dab..2964172d 100644 --- a/src/a2a/utils/errors.py +++ b/src/a2a/utils/errors.py @@ -1,3 +1,5 @@ +"""Custom exceptions for A2A server-side errors.""" + from a2a.types import ( ContentTypeNotSupportedError, InternalError, @@ -19,16 +21,27 @@ class A2AServerError(Exception): class MethodNotImplementedError(A2AServerError): - """Exception for Unimplemented methods.""" + """Exception raised for methods that are not implemented by the server handler.""" def __init__( self, message: str = 'This method is not implemented by the server' ): + """Initializes the MethodNotImplementedError. + + Args: + message: A descriptive error message. + """ self.message = message super().__init__(f'Not Implemented operation Error: {message}') class ServerError(Exception): + """Wrapper exception for A2A or JSON-RPC errors originating from the server's logic. + + This exception is used internally by request handlers and other server components + to signal a specific error that should be formatted as a JSON-RPC error response. + """ + def __init__( self, error: ( @@ -47,4 +60,10 @@ def __init__( | None ), ): + """Initializes the ServerError. + + Args: + error: The specific A2A or JSON-RPC error model instance. + If None, an `InternalError` will be used when formatting the response. + """ self.error = error diff --git a/src/a2a/utils/helpers.py b/src/a2a/utils/helpers.py index 648dd248..4e3228b2 100644 --- a/src/a2a/utils/helpers.py +++ b/src/a2a/utils/helpers.py @@ -1,5 +1,9 @@ +"""General utility functions for the A2A Python SDK.""" + import logging +from collections.abc import Callable +from typing import Any from uuid import uuid4 from a2a.types import ( @@ -21,7 +25,16 @@ @trace_function() def create_task_obj(message_send_params: MessageSendParams) -> Task: - """Create a new task object from message send params.""" + """Create a new task object from message send params. + + Generates UUIDs for task and context IDs if they are not already present in the message. + + Args: + message_send_params: The `MessageSendParams` object containing the initial message. + + Returns: + A new `Task` object initialized with 'submitted' status and the input message in history. + """ if not message_send_params.message.contextId: message_send_params.message.contextId = str(uuid4()) @@ -35,7 +48,15 @@ def create_task_obj(message_send_params: MessageSendParams) -> Task: @trace_function() def append_artifact_to_task(task: Task, event: TaskArtifactUpdateEvent) -> None: - """Helper method for updating Task with new artifact data.""" + """Helper method for updating a Task object with new artifact data from an event. + + Handles creating the artifacts list if it doesn't exist, adding new artifacts, + and appending parts to existing artifacts based on the `append` flag in the event. + + Args: + task: The `Task` object to modify. + event: The `TaskArtifactUpdateEvent` containing the artifact data. + """ if not task.artifacts: task.artifacts = [] @@ -82,14 +103,35 @@ def append_artifact_to_task(task: Task, event: TaskArtifactUpdateEvent) -> None: def build_text_artifact(text: str, artifact_id: str) -> Artifact: - """Helper to convert agent text to artifact.""" + """Helper to create a text artifact. + + Args: + text: The text content for the artifact. + artifact_id: The ID for the artifact. + + Returns: + An `Artifact` object containing a single `TextPart`. + """ text_part = TextPart(text=text) part = Part(root=text_part) return Artifact(parts=[part], artifactId=artifact_id) -def validate(expression, error_message=None): - """Decorator that validates if the given expression evaluates to True.""" +def validate( + expression: Callable[[Any], bool], error_message: str | None = None +): + """Decorator that validates if a given expression evaluates to True. + + Typically used on class methods to check capabilities or configuration + before executing the method's logic. If the expression is False, + a `ServerError` with an `UnsupportedOperationError` is raised. + + Args: + expression: A callable that takes the instance (`self`) as its argument + and returns a boolean. + error_message: An optional custom error message for the `UnsupportedOperationError`. + If None, the string representation of the expression will be used. + """ def decorator(function): def wrapper(self, *args, **kwargs): @@ -107,10 +149,23 @@ def wrapper(self, *args, **kwargs): def are_modalities_compatible( - server_output_modes: list[str], client_output_modes: list[str] -): - """Modalities are compatible if they are both non-empty - and there is at least one common element. + server_output_modes: list[str] | None, client_output_modes: list[str] | None +) -> bool: + """Checks if server and client output modalities (MIME types) are compatible. + + Modalities are compatible if: + 1. The client specifies no preferred output modes (client_output_modes is None or empty). + 2. The server specifies no supported output modes (server_output_modes is None or empty). + 3. There is at least one common modality between the server's supported list and the client's preferred list. + + Args: + server_output_modes: A list of MIME types supported by the server/agent for output. + Can be None or empty if the server doesn't specify. + client_output_modes: A list of MIME types preferred by the client for output. + Can be None or empty if the client accepts any. + + Returns: + True if the modalities are compatible, False otherwise. """ if client_output_modes is None or len(client_output_modes) == 0: return True diff --git a/src/a2a/utils/message.py b/src/a2a/utils/message.py index a984e557..2e156f6e 100644 --- a/src/a2a/utils/message.py +++ b/src/a2a/utils/message.py @@ -1,3 +1,5 @@ +"""Utility functions for creating and handling A2A Message objects.""" + import uuid from a2a.types import ( @@ -13,7 +15,18 @@ def new_agent_text_message( context_id: str | None = None, task_id: str | None = None, ) -> Message: - """Creates a new agent text message.""" + """Creates a new agent message containing a single TextPart. + + Args: + text: The text content of the message. + context_id: The context ID for the message. + task_id: The task ID for the message. + final: Optional boolean indicating if this is the final message. + metadata: Optional metadata for the message. + + Returns: + A new `Message` object with role 'agent'. + """ return Message( role=Role.agent, parts=[Part(root=TextPart(text=text))], @@ -25,9 +38,21 @@ def new_agent_text_message( def new_agent_parts_message( parts: list[Part], - context_id: str | None, + context_id: str | None = None, task_id: str | None = None, ): + """Creates a new agent message containing a list of Parts. + + Args: + parts: The list of `Part` objects for the message content. + context_id: The context ID for the message. + task_id: The task ID for the message. + final: Optional boolean indicating if this is the final message. + metadata: Optional metadata for the message. + + Returns: + A new `Message` object with role 'agent'. + """ return Message( role=Role.agent, parts=parts, @@ -38,9 +63,25 @@ def new_agent_parts_message( def get_text_parts(parts: list[Part]) -> list[str]: - """Return all text parts from a list of parts.""" + """Extracts text content from all TextPart objects in a list of Parts. + + Args: + parts: A list of `Part` objects. + + Returns: + A list of strings containing the text content from any `TextPart` objects found. + """ return [part.root.text for part in parts if isinstance(part.root, TextPart)] def get_message_text(message: Message, delimiter='\n') -> str: + """Extracts and joins all text content from a Message's parts. + + Args: + message: The `Message` object. + delimiter: The string to use when joining text from multiple TextParts. + + Returns: + A single string containing all text content, or an empty string if no text parts are found. + """ return delimiter.join(get_text_parts(message.parts)) diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index 52f9596a..9cf4df43 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -1,9 +1,21 @@ +"""Utility functions for creating A2A Task objects.""" + import uuid from a2a.types import Artifact, Message, Task, TaskState, TaskStatus def new_task(request: Message) -> Task: + """Creates a new Task object from an initial user message. + + Generates task and context IDs if not provided in the message. + + Args: + request: The initial `Message` object from the user. + + Returns: + A new `Task` object initialized with 'submitted' status and the input message in history. + """ return Task( status=TaskStatus(state=TaskState.submitted), id=(request.taskId if request.taskId else str(uuid.uuid4())), @@ -20,6 +32,20 @@ def completed_task( artifacts: list[Artifact], history: list[Message] | None = None, ) -> Task: + """Creates a Task object in the 'completed' state. + + Useful for constructing a final Task representation when the agent + finishes and produces artifacts. + + Args: + task_id: The ID of the task. + context_id: The context ID of the task. + artifacts: A list of `Artifact` objects produced by the task. + history: An optional list of `Message` objects representing the task history. + + Returns: + A `Task` object with status set to 'completed'. + """ if history is None: history = [] return Task( diff --git a/src/a2a/utils/telemetry.py b/src/a2a/utils/telemetry.py index 29843f74..0aeee931 100644 --- a/src/a2a/utils/telemetry.py +++ b/src/a2a/utils/telemetry.py @@ -86,9 +86,9 @@ def trace_function( exceptions that occur. It can be used in two ways: + 1. As a direct decorator: `@trace_function` - 2. As a decorator factory to provide arguments: - `@trace_function(span_name="custom.name")` + 2. As a decorator factory to provide arguments: `@trace_function(span_name="custom.name")` Args: func (callable, optional): The function to be decorated. If None, @@ -221,18 +221,20 @@ def trace_class( This decorator iterates over the methods of a class and applies the `trace_function` decorator to them, based on the `include_list` and - `exclude_list` criteria. Dunder methods (e.g., `__init__`, `__call__`) - are always excluded. + `exclude_list` criteria. Methods starting or ending with double underscores + (dunder methods, e.g., `__init__`, `__call__`) are always excluded by default. Args: include_list (list[str], optional): A list of method names to explicitly include for tracing. If provided, only methods in this list (that are not dunder methods) will be traced. - Defaults to None. + Defaults to None (trace all non-dunder methods). exclude_list (list[str], optional): A list of method names to exclude from tracing. This is only considered if `include_list` is not provided. Dunder methods are implicitly excluded. Defaults to an empty list. + kind (SpanKind, optional): The `opentelemetry.trace.SpanKind` for the + created spans on the methods. Defaults to `SpanKind.INTERNAL`. Returns: callable: A decorator function that, when applied to a class,