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:
- Graph type β a class extending
AgentGraph - State type β a class extending
AgentState
What Happens on Instantiationβ
When you create an AgentApplication, it automatically:
- Loads the Agent Card from
./agent.jsonand the Agent Configuration from./config.yaml- When loading the Agent Card, it expects the
supportedInterfacesfield to be empty, as it will configure it with the endpoint specified inconfig.yaml'sendpointsection (urlandport).
- When loading the Agent Card, it expects the
- 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.yamlor the path set in theCONFIG_PATHenvironment variable when the AgentApplication starts - Validated by checking connectivity to all
requiredMCP 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
astreammethod of the AgentGraph. - Processes each chunk individually, converting
AgentTaskResultobjects 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.
- Calls the
-
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:
- Extend the
AgentGraphclass - 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_builderproperty.
- Instantiate all ChatModelClient instances your agent will use as properties of the extended class (e.g.
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 anAgentTaskResult
Optional overrides for advanced features:
update_after_checkpoint_restore(str)β for multi-turn conversationsis_waiting_for_human_input()β for Human-in-the-Loop (HITL) interactions
How State Worksβ
- Each node in the Agent Graph receives the current
AgentStateand returns an updatedAgentState - The Agent Graph converts the updated state into an AgentTaskResult using
to_task_result()in theastreammethod. - The Agent Executor then processes the
AgentTaskResultand 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
invokefor single requests,ainvokefor the same request from async code (it awaits the model instead of blocking the event loop), andbatchfor 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
- Similar to
-
_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
AgentStatesubclass you defined) - A
ChatModelClientwith tools enabled (created inside theAgentGraph.setupmethod) - 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
type | Destination |
|---|---|
remote | OTLP/HTTP collector, e.g. Arize Phoenix. endpoint defaults to http://localhost:6006 |
local | JSON Lines file, one span per line. file_path defaults to spans.jsonl |
console | stdout, 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:
| Level | Redacts |
|---|---|
none | nothing β the default |
content | prompts, 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
CLIENTspan around the remote call, injectingtraceparentinto the outgoing message. It also records what only the caller sees: the request text (input.value), the last answer text (output.value).