Skip to main content
Version: v6.0.0 [Falcon]

High Level documentation of BAT-ADK

Introduction​

BAT-ADK (BubbleRAN Agent Development Kit) is a Python SDK built on top of LangGraph that helps you build intelligent AI agents with minimal effort. It natively supports two standard protocols:

  • A2A (Agent-to-Agent): for JSONRPC 2.0 communication between agents (A2A version >= 1.0)
  • MCP (Model Context Protocol): for accessing external tools, prompts, and contextual data

Protocols at a Glance​

  • A2A is the protocol for agent-to-agent communication.
  • MCP is used by agents to securely interact with external resources such as tools and prompts.

Design Philosophy​

BAT-ADK is designed to let you focus on agent behavior and workflows, not protocol details.
You define your agent’s logic using graph-based workflows, while the SDK handles communication, protocol compliance, and integration under the hood.

Preliminary Concepts​

Before using BAT-ADK, you should be familiar with a few core ideas:

  • LangGraph basics
    Understand how to define a graph, manage graph state, use streaming, and handle interrupts for Human-in-the-Loop (HITL) interactions.

  • A2A fundamentals
    Know the basics of the A2A protocol, especially what an Agent Card is and its role in agent communication.

  • MCP fundamentals
    Have a basic understanding of the Model Context Protocol (MCP) and how it enables agents to access tools, prompts, and context.

Agent Application​

An Agent Application is the entry point object for agents built with BAT-ADK. It is an instance of the AgentApplication class and requires two parameters:

  1. Graph type – a class extending AgentGraph
  2. State type – a class extending AgentState

What Happens on Instantiation​

When you create an AgentApplication, it automatically:

  • Loads the Agent Card from ./agent.json and the Agent Configuration from ./config.yaml
    • When loading the Agent Card, it expects the supportedInterfaces field to be empty, as it will configure it with the endpoint specified in config.yaml's endpoint section (url and port).
  • Instantiates the AgentGraph
  • Sets up the AgentExecutor, Request Handler, and a Starlette web application

Running the Agent Application​

After creation, call the run() method of the Agent Application to start the A2A Server.

Ports​

  • A2A: 9900 (default)

You can override these in config.yaml via endpoint.port.

Agent Configuration​

The AgentConfig defines how an agent behaves and what external resources it can access, including:

  • The endpoint (URL and ports) the agent binds to
  • The default model (provider, name, base URL) for the agent's clients
  • Whether to perform checkpoints
  • Telemetry (OpenTelemetry) settings β€” see Telemetry
  • Which MCP Servers and other Agents it needs to communicate with

Loading and Validation​

  • Automatically loaded from config.yaml or the path set in the CONFIG_PATH environment variable when the AgentApplication starts
  • Validated by checking connectivity to all required MCP Servers and Agents
    • If any required resource is unreachable, the application crashes and must be restarted (handled automatically in Kubernetes)

Features​

The AgentConfig class provides methods to:

  • List available MCP Servers and Agents
  • Retrieve the AgentCard of specific Agents
  • Retrieve the Tools provided by specific MCP Servers

These methods help you integrate external resources directly into your agent’s Graph logic.

Naming Guidelines​

  • In the configuration, you assign a name and a URL for each MCP Server or Agent.
  • Always use the Official name of the server or agent in your agent logic, not the name you assigned in the config.
  • The SDK automatically maps your assigned names to the Official names during validation.
  • Using Official names ensures your agent will work correctly when deployed with tools like the AIFabric controller of the Orama Operator.

Example:

remote-agents:
- name: MyHelperAgent # your assigned name
url: http://helper:9900

In your agent code, reference this agent by its Official name, e.g., HelperAgent, not MyHelperAgent.

Agent Executor​

The Agent Executor handles task execution and event publishing for an agent.

Key Functions​

  • Execute a request

    • Calls the astream method of the AgentGraph.
    • Processes each chunk individually, converting AgentTaskResult objects into A2A events for the event queue.
    • When telemetry is enabled, wraps the streaming loop in a root OpenTelemetry span (invoke_agent). Token usage and tool calls are captured through OpenTelemetry rather than collected by hand into the stream β€” see Telemetry.
  • Cancel a request

    • Currently not implemented

Agent Graph​

An AgentGraph defines the core logic of an agent using the LangGraph library. Agents built with BAT-ADK stream their responses as AgentTaskResult objects, produced after executing each node in the graph.

Creating a Custom Graph​

You cannot instantiate AgentGraph directly. Instead:

  1. Extend the AgentGraph class
  2. Implement the setup(config: AgentConfig) method. Inside this method:
    • Instantiate all ChatModelClient instances your agent will use as properties of the extended class (e.g. self.<client-name>).
    • Instantiate any prebuilt workflows.
    • Define the graph’s nodes and edges via the graph_builder property.

After setup completes, the AgentGraph is compiled by the BAT-ADK (you don't need to do it manually) and ready to use.

Streaming Responses​

The AgentGraph class provides an astream method, which the Agent Executor uses to submit requests and receive streamed responses.

Agent State​

Each AgentGraph has a State that updates after each node executes. The state is defined by extending the AgentState class, which is a Pydantic model ensuring type safety.

Implementing a Custom State​

When you extend AgentState, you must:

  • Override from_query(str) – to initialize the state from a query
  • Override to_task_result() – to convert the state into an AgentTaskResult

Optional overrides for advanced features:

  • update_after_checkpoint_restore(str) – for multi-turn conversations
  • is_waiting_for_human_input() – for Human-in-the-Loop (HITL) interactions

How State Works​

  • Each node in the Agent Graph receives the current AgentState and returns an updated AgentState
  • The Agent Graph converts the updated state into an AgentTaskResult using to_task_result() in the astream method.
  • The Agent Executor then processes the AgentTaskResult and generates A2A events for the event queue

Chat Model Client​

A Chat Model Client is a wrapper around an LLM that combines:

  • A LangChain BaseChatModel
  • System instructions
  • Optional tools

What It Does​

  • Provides invoke for single requests, ainvoke for the same request from async code (it awaits the model instead of blocking the event loop), and batch for parallel requests

Token usage (input/output/total tokens) and LLM timing are captured through OpenTelemetry: when telemetry is enabled, OpenInference auto-instrumentation records them on the underlying model's spans, so the client no longer collects them by hand. See Telemetry for how the data is exported and read back.

Configuration​

A ChatModelClient is configured using a ChatModelClientConfig. Its defaults (provider, model name, base URL) come from config.yaml's model section, while the MODEL / MODEL_PROVIDER / BASE_URL environment variables still override them.
The configuration includes:

  • Model provider and model name
  • Optional LLM endpoint URL
  • Optional client name (developer-defined identifier)

Structured Output​

A ChatModelClient can be configured to provide a Pydantic Model as output of invoke and ainvoke by setting the output_schema parameter in the ChatModelClient constructor.

Note: this feature is currently not supported when using the ChatModelClient with tools or in a ReAct Loop. Support will be provided in the near future.

Prebuilt Workflows​

Prebuilt Workflows are reusable Runnable components that can be used as nodes inside an AgentGraph.

Purpose​

They help you encapsulate common or complex logic into reusable building blocks, making agent graphs easier to design and maintain.

Creating a Prebuilt Workflow​

BAT-ADK provides the abstract PrebuiltWorkflow class. To create one, you must extend it and implement:

  • _setup()

    • Similar to AgentGraph.setup
    • Used to initialize models, tools, and internal state
  • _astream()

    • Implements the workflow’s custom streaming logic
    • This is more advanced and requires careful handling

Using a Prebuilt Workflow​

Once defined, call as_runnable() to obtain a Runnable that can be directly used as a node in an Agent Graph.

ReAct Loop​

The ReAct Loop is a prebuilt workflow that implements the classic ReAct pattern (Reason + Act) using:

  • An LLM node
  • A Tool node

It lets you add a full ReAct workflow to an Agent Graph as a single node.

How to Use It​

To instantiate a ReAct Loop, you must provide:

  • The Agent State schema used by your graph (the AgentState subclass you defined)
  • A ChatModelClient with tools enabled (created inside the AgentGraph.setup method)
  • The names of specific state fields:
    • Where the loop reads its input from
    • Where the loop writes its output to

Check the documentation for the full list of available parameters.

The LLM node calls the client's ainvoke. A client that customises the model call (a budget, a short-circuit) overrides ainvoke, not invoke.

This design keeps the ReAct logic reusable while cleanly integrating with your agent’s state and graph.

Call Agent Node​

The Call Agent Node is a prebuilt workflow that abstracts the Agent-to-Agent (A2A) streaming.

It lets you integrate a remote agent call into your graph as a single node, handling the stream consumption and state updates automatically. This feature allows for real-time streaming of what the called agent is doing.

How to Use It​

To instantiate a Call Agent Node, you must provide:

  • The Agent Config and Agent State schema
  • The target agent's name (the agent must be specified in the Agent Config)
  • A Message Builder function to construct the request from the current state
  • The names of some specific state fields:
    • Where to store the remote agent's status and content
    • Where to flag if the remote agent needs human input

When telemetry is enabled, this node propagates the trace context (W3C traceparent) to the remote agent so its spans join the same trace; the remote agent's token usage is then recomposed from those spans rather than merged into local metrics by the node. See Telemetry.

Telemetry​

BAT-ADK exports its internals as OpenTelemetry spans: token usage, tool calls, LLM timing and the shape of the graph. An agent calling another propagates the W3C traceparent through the A2A message, so both agents' spans land in the same trace despite running as separate processes.

It lives behind an extra, so an agent that does not want it does not pay for it:

dependencies = ["bat-adk[telemetry]"]   # or bat-adk[all]

Turning It On​

Everything is configured in config.yaml, under telemetry. Telemetry is on as soon as output has at least one entry, and spans fan out to every entry, so a run can go to a collector and a file at once.

telemetry:
# service_name: my-agent # optional; defaults to the agent card name
# project_name: my-agent # optional; Phoenix project (default: "default")
privacy: none # none | content | names | full
output:
- type: remote
endpoint: http://localhost:6006
- type: local
file_path: spans.jsonl
typeDestination
remoteOTLP/HTTP collector, e.g. Arize Phoenix. endpoint defaults to http://localhost:6006
localJSON Lines file, one span per line. file_path defaults to spans.jsonl
consolestdout, for debugging

An unknown type is skipped with a warning rather than disabling the whole pipeline.

project_name is distinct from service_name: the first is the project the trace is filed under, the second labels the spans within it. Agents that share a distributed trace must use the same project_name, or the trace fragments across projects.

Privacy​

By default a span carries prompts, completions and tool definitions in full. telemetry.privacy decides how much of that may leave the process. It is a single ordered dial, and each level redacts everything the level below it does:

LevelRedacts
nonenothing β€” the default
contentprompts, messages, completions, invocation parameters, every tool's description, parameter schema and call arguments, what one agent asked another and its answer (input.value / output.value), and error text.

Redacted values are replaced with __REDACTED__ before any exporter sees them; they never leave the process.

Token counts, span kinds, hierarchy and timing survive at every level, so cost accounting keeps working even at full. What full costs you is anything that reconstructs tool calls from spans: those metrics key off the tool name and go empty. Use it when the tool inventory itself is considered proprietary.

The level is written as its name (content) or its ordinal (1). An unknown value is a validation error, not a silent fallback to none: a typo must not export in the clear precisely when someone was trying to lock the agent down.

Privacy Floor​

config.yaml sits beside the running agent and can be replaced there β€” a mounted ConfigMap, a swapped file, or CONFIG_PATH pointing elsewhere. On its own it is a default, not a guarantee.

An agent shipped as a packaged binary can set a floor in its own source, which is frozen into the binary along with the rest of the agent code:

agent = AgentApplication(
AgentGraphType=MyAgentGraph,
AgentStateType=MyAgentState,
telemetry_privacy_floor="content",
)

The effective level is the higher of the two, so a replaced config.yaml can raise privacy but never lower it. The default floor is none, which leaves config.yaml to decide alone.

What Gets Instrumented​

  • LangChain / LangGraph β€” automatically, through OpenInference: token counts, prompts, completions and tool calls.
  • Agent Executor β€” one root invoke_agent <AgentName> span per request, carrying the conversation and task ids, continuing an incoming trace when there is one.
  • Call Agent Node β€” a CLIENT span around the remote call, injecting traceparent into the outgoing message. It also records what only the caller sees: the request text (input.value), the last answer text (output.value).