FastA2A

repository·main·Indexed 42 days ago

https://github.com/datalayer/fasta2a

An agentic framework-agnostic implementation of the A2A protocol in Python. It provides an ASGI-compatible server to convert AI Agents into A2A servers, featuring a decoupled architecture with TaskManager, Broker, Storage, and Worker components. It includes built-in support for Pydantic AI 2 via the agent_to_a2a helper and supports extension negotiation via URIs.

Tokens
12.2K
Snippets
31
Records
47
Agent score
53%

What's inside FastA2A

  1. Understand the FastA2A Architecture

    main

    FastA2A is built on Starlette and uses a decoupled architecture to separate the web server from task execution. The core components are:

    • TaskManager: Coordinates the flow, interacts with Storage, and schedules tasks via the Broker.
    • Broker: A queueing/scheduling mechanism that receives tasks from the TaskManager and delegates them to a Worker.
    • Storage: The persistence layer for both tasks and conversation context.
    • Worker: The implementation layer that actually executes the agent logic.

    This design allows the Worker to run on a different machine or in a different process than the HTTP server.

  2. How Tasks and Context work in FastA2A

    main

    FastA2A follows an opinionated model where every incoming message triggers a background Task executed by a Worker.

    Tasks

    A Task represents a single complete execution of an agent. When a client sends a message, a new task is created. The agent runs until it reaches a final state (e.g., completed, failed, canceled). The final output of the execution is stored as a task artifact.

    Context

    Context represents a conversation thread that can span multiple tasks. It is managed via a context_id:

    • New Conversations: If a message is sent without a context_id, the server generates a new one.
    • Continuing Conversations: Subsequent messages including the same context_id allow the agent to access the complete message history from previous tasks.

    Storage Roles

    The Storage component manages two distinct types of data:

    1. Task Storage: Persists tasks in the A2A protocol format (status, artifacts, message history).
    2. Context Storage: Persists conversation context in a format optimized for your specific agent implementation (e.g., storing internal reasoning traces or tool call states).
  3. Use development scripts for setup and testing

    main

    The repository provides a set of standardized scripts located in the scripts/ directory to manage the development lifecycle. Use these scripts to install dependencies, run tests, format code, and perform linting/type checks.

    Available scripts:

    • scripts/setup: Installs necessary dependencies.
    • scripts/test: Executes the test suite.
    • scripts/lint: Runs the code formatter.
    • scripts/check: Runs the linter in check mode and executes the type checker.
  4. Implement a custom Worker for FastA2A

    main

    To use FastA2A with your own agentic framework, you must implement the Worker class. The Worker is responsible for executing tasks and typically runs in a separate process or machine from the web server.

    Key responsibilities of a Worker implementation:

    1. run_task(params: TaskSendParams): Load the task from storage, update its state to working, load the conversation context, execute your agent logic, and finally update the task with the new message and artifacts while setting the state to completed.
    2. cancel_task(params: TaskIdParams): Handle task cancellation.
    3. build_message_history(history: list[Message]): Transform stored messages into your agent's internal history format.
    4. build_artifacts(result: Any): Convert agent results into a list of Artifact objects.

    You can use InMemoryStorage and InMemoryBroker for local development.

    import uuid
    from collections.abc import AsyncIterator
    from contextlib import asynccontextmanager
    from typing import Any
    
    from fasta2a import FastA2A, Worker
    from fasta2a.broker import InMemoryBroker
    from fasta2a.schema import Artifact, Message, TaskIdParams, TaskSendParams, TextPart
    from fasta2a.storage import InMemoryStorage
    
    Context = list[Message]
    
    class InMemoryWorker(Worker[Context]):
        async def run_task(self, params: TaskSendParams) -> None: 
            # Implementation logic here...
            pass
    
        async def cancel_task(self, params: TaskIdParams) -> None: ...
    
        def build_message_history(self, history: list[Message]) -> list[Any]: ...
    
        def build_artifacts(self, result: Any) -> list[Artifact]: ...
    
    storage = InMemoryStorage()
    broker = InMemoryBroker()
    worker = InMemoryWorker(storage=storage, broker=broker)
    
    @asynccontextmanager
    async def lifespan(app: FastA2A) -> AsyncIterator[None]:
        async with app.task_manager:
            async with worker.run():
                yield
    
    app = FastA2A(storage=storage, broker=broker, lifespan=lifespan)
  5. Configure and use A2A Extensions

    main

    Extensions allow agents to declare and negotiate additional capabilities via URIs.

    1. Declare Extensions: Pass a list of AgentExtension objects to the FastA2A constructor or to agent_to_a2a.
    2. Access Activated Extensions: Inside your Worker.run_task method, use activated_extensions(params) to check which extensions the client successfully negotiated. If a client requests an extension that was marked as required=True but is not provided, the request will be refused with a -32600 error.

    Example declaration:

    from fasta2a import AgentExtension, FastA2A
    
    app = FastA2A(
        storage=storage,
        broker=broker,
        extensions=[
            AgentExtension(uri='https://example.com/ext/citations/v1', description='Cites its sources'),
            AgentExtension(uri='https://example.com/ext/trace/v1', required=True),
        ],
    )

    Example usage in a Worker:

    from fasta2a import activated_extensions
    
    class MyWorker(Worker[Context]):
        async def run_task(self, params: TaskSendParams) -> None:
            if 'https://example.com/ext/citations/v1' in activated_extensions(params):
                # Add citation parts to artifacts
                pass
  6. Integrate Pydantic AI with FastA2A

    main

    You can quickly turn a pydantic_ai.Agent into an A2A-compatible ASGI application using the agent_to_a2a helper.

    First, install the integration extra:

    pip install 'fasta2a[pydantic-ai]'

    Then use the following pattern:

    from pydantic_ai import Agent
    from fasta2a.pydantic_ai import agent_to_a2a
    
    agent = Agent('openai:gpt-5.5')
    app = agent_to_a2a(agent)

    Note: This requires Pydantic AI 2. The older Agent.to_a2a() method has been replaced by agent_to_a2a.

  7. Core components of FastA2A

    main

    The fasta2a package provides a framework for agent-to-agent (A2A) communication and task execution. The primary exported components are:

    • FastA2A: The main application class used to orchestrate agent activities.
    • Broker: Manages the routing and distribution of messages/tasks between agents.
    • Worker: Executes specific tasks or skills assigned by the broker.
    • Storage: Handles persistence of state, messages, or task data.
    • Skill: A discrete capability or function that an agent can perform.
    • AgentExtension: A mechanism to extend agent functionality.
    • A2A_EXTENSIONS_HEADER: A constant used for identifying extensions in communication headers.
    • activated_extensions: A utility to retrieve currently active extensions.
  8. How extension negotiation works in FastA2A

    main

    FastA2A supports per-request extension negotiation.

    1. Request: A client specifies the extensions it wants in the A2A-Extensions header.
    2. Negotiation: The FastA2A instance parses this header and selects only the extensions that the agent actually supports (_negotiate_extensions).
    3. Validation: If a request uses a method like message/send or message/stream that requires specific extensions, and those extensions were not successfully negotiated, the server returns an InvalidRequestError (code -32600) before creating a task.
    4. Response: The server includes the list of activated extensions in the A2A-Extensions header of its response. These activated extensions are also injected into the message metadata under the ACTIVATED_EXTENSIONS_KEY so that the background worker knows which extensions are active for that specific task.
  9. Configure Agent Security Schemes

    main

    Security is defined using SecurityScheme and SecurityRequirement. A SecurityScheme is a wrapper that can contain one of several authentication types:

    • http_auth_security_scheme: For HttpSecurityScheme (e.g., 'Bearer', 'Basic').
    • api_key_security_scheme: For ApiKeySecurityScheme (specifies name and location like 'header' or 'query').
    • oauth2_security_scheme: For OAuth2SecurityScheme (includes supported OAuthFlows).
    • open_id_connect_security_scheme: For OpenIdConnectSecurityScheme.
    • mtls_security_scheme: For Mutual TLS.

    SecurityRequirement maps scheme names to required scopes: dict[str, list[str]].

  10. Understand Tasks, Messages, and Artifacts

    main

    The A2A protocol revolves around three core entities:

    1. Task: A stateful entity representing a specific outcome. It has a status (e.g., working, completed, failed) and contains a history of Messages and a collection of Artifacts.
    2. Message: Communication content that is NOT an artifact. Used for agent thoughts, user context, or status updates. Messages consist of Parts (text, raw bytes, or structured JSON data).
    3. Artifact: Immutable results generated by an agent (e.g., a file or a webpage). Artifacts can have multiple Parts and are often the end result of a Task.

    Task States (TaskState): submitted, working, input-required, completed, canceled, failed, rejected, auth-required.

  11. How the Worker lifecycle and task handling works

    main

    The Worker follows a specific lifecycle to ensure tasks are correctly tracked in storage and streams are closed properly:

    1. Startup: Calling async with worker.run(): starts an internal loop that listens for TaskOperations (like run or cancel) from the broker.
    2. Execution: When a run operation is received, the worker calls run_task.
    3. Error Handling: If run_task raises an exception, the worker automatically marks the task as failed in storage and closes the stream.
    4. Completion: When run_task returns successfully, the worker reads the final state from storage. If the state is one of the STREAM_ENDING_STATES, the worker publishes the final state and closes the stream.
    5. Cleanup: The run() context manager ensures that the task group is cancelled when the block exits.