Skip to navigation

Framework Adapters

LangGraph, Pydantic AI, CrewAI, Google ADK, Agno, Strands, NOOA, Letta, and Parlant adapters

These adapters wrap an agent framework’s own loop: the framework owns the model and the reasoning, and the adapter feeds it Band’s history and tools. Every adapter follows the shared adapter construction rules and takes the feature keywords; only adapter-specific settings are listed here.

LangGraphAdapter

Adapter for LangGraph agents.

from band.adapters import LangGraphAdapter, LangGraphAdapterConfig
adapter = LangGraphAdapter(
config: LangGraphAdapterConfig | None = None,
*,
history_converter: LangChainHistoryConverter | None = None,
additional_tools: list[Any] | None = None,
llm: BaseChatModel | None = None,
checkpointer: BaseCheckpointSaver | None = None,
graph_factory: Callable[[list[Any]], Pregel] | None = None,
graph: Pregel | None = None,
**features: Unpack[FeatureKwargs],
)
ArgumentTypeRequiredDescription
llmBaseChatModelConditionalLangChain chat model, such as ChatOpenAI
checkpointerBaseCheckpointSaverNoLangGraph checkpointer for state; the llm pattern falls back to InMemorySaver
graph_factoryCallable[[list[Any]], Pregel]ConditionalCustom graph factory, called with the tool list
graphPregelConditionalStatic graph instance
additional_toolslist[Any]NoBand (InputModel, handler) pairs or LangChain tools
LangGraphAdapterConfig fieldTypeDefaultDescription
prompt_templatestr"default"System prompt template
custom_sectionstr""Additional instructions added to the system prompt
recursion_limitint50Maximum graph recursion steps; must be greater than 0
inject_system_promptbool | NoneNoneWhether to prepend the Band system prompt on session bootstrap. None is on for the llm pattern and off for graph_factory / graph, which usually manage their own system messages
from langchain_openai import ChatOpenAI
from band.adapters import LangGraphAdapter
adapter = LangGraphAdapter(llm=ChatOpenAI(model="gpt-5.4-mini"))

Provide either llm for the simple pattern or graph_factory / graph for your own graph. With none of them, the constructor raises ValueError: Must provide either llm (simple pattern) or graph_factory/graph (advanced pattern). Emits TOOL_CALLS and USAGE. See LangGraph for a worked example.

PydanticAIAdapter

Adapter for Pydantic AI agents with type-safe tools.

from band.adapters import PydanticAIAdapter, PydanticAIAdapterConfig
adapter = PydanticAIAdapter(
config: PydanticAIAdapterConfig | None = None,
*,
history_converter: PydanticAIHistoryConverter | None = None,
additional_tools: list[Callable[..., Any] | CustomToolDef] | None = None,
llm: Model | None = None,
instrument: bool | InstrumentationSettings | None = None,
**features: Unpack[FeatureKwargs],
)
SettingWhereTypeRequiredDescription
modelconfigstr | NoneConditionalModel in provider:model format, such as "openai:gpt-5.4-mini". Since Pydantic AI 2.0 the bare openai: prefix routes to the Responses API; use openai-chat: for Chat Completions
llmargumentModel | NoneConditionalA constructed Pydantic AI Model, used instead of config.model
system_promptconfigstr | NoneNoReplaces the rendered Band system prompt entirely
custom_sectionconfigstr | NoneNoAdditional instructions appended to the rendered prompt
instrumentargumentbool | InstrumentationSettings | NoneNoOpenTelemetry instrumentation for the underlying Pydantic AI agent. None inherits the host’s Agent.instrument_all(), False opts out, True enables Pydantic AI’s defaults, and an InstrumentationSettings customizes them. Band never creates a tracer provider or exporter
from band.adapters import PydanticAIAdapter, PydanticAIAdapterConfig
adapter = PydanticAIAdapter(PydanticAIAdapterConfig(model="openai:gpt-5.4-mini"))

Set exactly one of config.model and llm; otherwise the constructor raises ValueError: Set exactly one of config.model or llm. Emits TOOL_CALLS and USAGE. Since band-sdk 5.1.0 the adapter ends the Pydantic AI run after a successful band_no_reply call whose sibling tool calls also succeeded, instead of letting the model continue. See Pydantic AI for a worked example.

CrewAIAdapter

Adapter for CrewAI agents with role, goal, and backstory definitions.

from band.adapters import CrewAIAdapter, CrewAIAdapterConfig
adapter = CrewAIAdapter(
config: CrewAIAdapterConfig | None = None,
*,
history_converter: CrewAIHistoryConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
**features: Unpack[FeatureKwargs],
)
CrewAIAdapterConfig fieldTypeDefaultDescription
modelstr"gpt-6-luna"Model name for the CrewAI LLM
rolestr | NoneNoneAgent’s role; defaults to the agent name
goalstr | NoneNoneAgent’s primary objective; defaults to the agent description
backstorystr | NoneNoneAgent background and expertise; the platform instructions are appended to it
custom_sectionstr | NoneNoneAdditional instructions added to the platform prompt
verboseboolFalseEnable detailed CrewAI logging
max_iterint20Maximum agent iterations; must be greater than 0
max_rpmint | NoneNoneMaximum requests per minute; None is unlimited
allow_delegationboolFalseWhether to allow CrewAI task delegation
from band.adapters import CrewAIAdapter, CrewAIAdapterConfig
adapter = CrewAIAdapter(CrewAIAdapterConfig(role="Researcher", goal="Answer with sources"))

Emits TOOL_CALLS. system_prompt is not a field; use backstory. See CrewAI for a worked example.

CrewAIFlowAdapter

Runs one local CrewAI Flow execution per inbound room message and rebuilds orchestration state from task events.

from band.adapters import CrewAIFlowAdapter, CrewAIFlowAdapterConfig
adapter = CrewAIFlowAdapter(
config: CrewAIFlowAdapterConfig | None = None,
*,
history_converter: CrewAIFlowStateConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
flow_factory: Callable[[], Any],
state_source: CrewAIFlowStateSource | None = None,
**features: Unpack[FeatureKwargs],
)

flow_factory is required and keyword-only; it returns a new Flow for each message. A flow_factory that is not callable raises BandConfigError: flow_factory must be callable. state_source defaults to a REST-backed source that reads the run’s task events.

CrewAIFlowAdapterConfig fieldTypeDefaultDescription
join_policy"all" | "first""all"When a parallel delegation round counts as answered
metadata_namespacestr | NoneNoneTask-event metadata key holding the adapter’s state; None derives crewai_flow:<agent_id> at startup
max_delegation_roundsint4Delegation rounds a run may take, from 1 to 20
max_run_agetimedeltatimedelta(days=7)How long an unfinished run stays resumable; must be positive
text_only_behavior"error_event" | "fallback_send""error_event"What happens when the flow returns bare text instead of a decision
tagged_peer_policy"require_delegation_before_final" | "off""require_delegation_before_final"Whether tagged peers must be delegated to before a final answer
sequential_chainsdict[str, str]{}Upstream peer to downstream peer: once the upstream peer replies, synthesis waits until the downstream peer has been delegated to
accept_agent_initiatedboolFalseWhether messages from other agents may start a new run

Emits TOOL_CALLS. See CrewAI for a worked example.

GoogleADKAdapter

Adapter for Google Agent Development Kit agents.

from band.adapters import GoogleADKAdapter, GoogleADKAdapterConfig
adapter = GoogleADKAdapter(
config: GoogleADKAdapterConfig | None = None,
*,
history_converter: GoogleADKHistoryConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
**features: Unpack[FeatureKwargs],
)
GoogleADKAdapterConfig fieldTypeDefaultDescription
modelstr"gemini-3.8-flash"Gemini model name passed to the ADK agent
system_promptstr | NoneNoneReplaces the rendered Band system prompt entirely
custom_sectionstr | NoneNoneAdditional instructions appended to the rendered prompt
max_retriesint2Extra HTTP attempts per Gemini generation request, never a retry of a tool or a turn; 0 disables them. Must be from 0 to 10
max_history_messagesint50Most recent room messages replayed as a transcript at the start of each turn
max_transcript_charsint100_000Character cap on that transcript; older lines are dropped first
from band.adapters import GoogleADKAdapter, GoogleADKAdapterConfig
adapter = GoogleADKAdapter(GoogleADKAdapterConfig(custom_section="Keep answers short."))

Emits TOOL_CALLS and USAGE. See Google ADK for a worked example.

AgnoAdapter

Wraps an Agno agent you build. The adapter takes ownership of that instance.

from band.adapters import AgnoAdapter
adapter = AgnoAdapter(
config: AgnoAdapterConfig | None = None,
*,
history_converter: AgnoHistoryConverter | None = None,
agent: AgnoAgent,
session_id_factory: Callable[[str], str] = lambda room_id: room_id,
**features: Unpack[FeatureKwargs],
)

AgnoAdapterConfig has no fields: the Agno agent owns the model and the prompt. agent is required and keyword-only. session_id_factory maps a room ID to the Agno session ID; the default uses the room ID. There is no additional_tools argument; give tools to the Agno agent. Emits TOOL_CALLS, THOUGHTS, and USAGE. See Agno for a worked example.

StrandsAdapter

Adapter for Strands Agents.

from band.adapters import StrandsAdapter, StrandsAdapterConfig
adapter = StrandsAdapter(
config: StrandsAdapterConfig | None = None,
*,
history_converter: StrandsHistoryConverter | None = None,
additional_tools: list[Callable[..., Any] | CustomToolDef] | None = None,
llm: Model | None = None,
**features: Unpack[FeatureKwargs],
)
SettingWhereTypeRequiredDescription
modelconfigstr | NoneConditionalA Bedrock model identifier
llmargumentModel | NoneConditionalA constructed Strands Model, used instead of config.model
system_promptconfigstr | NoneNoReplaces the rendered Band system prompt entirely
custom_sectionconfigstr | NoneNoAdditional instructions appended to the rendered prompt
additional_toolsargumentlist[Callable | CustomToolDef]NoNamed functions, @strands.tool tools, or (InputModel, handler) pairs. A tool without a name raises ValueError: Custom tool <tool> has no name. Pass a named function, a @strands.tool-decorated tool, or a (InputModel, handler) pair.

Set exactly one of config.model and llm; otherwise the constructor raises ValueError: Set exactly one of config.model or llm. For OpenAI models, pass a Strands OpenAIResponsesModel as llm. After a successful band_no_reply call the adapter stops the Strands event loop once the current tool batch is recorded. Emits TOOL_CALLS and USAGE. See Strands Agents for a worked example.

NooaAdapter

Runs a NOOA (NVIDIA Object Oriented Agents) agent class in Band rooms, one instance per room. A NOOA agent is a Python class: its docstring is the prompt, its fields are state, its methods are tools, and the model writes and runs Python against self to act. The adapter adds Band’s tools as async methods on self.room, not as JSON schemas. Added in band-sdk 5.1.0.

from band.adapters import NooaAdapter, NooaAdapterConfig, NooaRoomAgent
adapter = NooaAdapter(
config: NooaAdapterConfig | None = None,
*,
agent: type[NooaRoomAgent],
history_converter: NooaHistoryConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
**features: Unpack[FeatureKwargs],
)
ArgumentTypeRequiredDescription
agenttype[NooaRoomAgent]YesThe agent class, not an instance; it must be constructible with no arguments. Anything else raises TypeError: agent must be a NooaRoomAgent subclass
additional_toolslist[CustomToolDef]No(InputModel, handler) pairs exposed on self.room beside the Band tools. Names must be public Python identifiers and must not collide with a Band tool
NooaAdapterConfig fieldTypeDefaultDescription
custom_sectionstr | NoneNoneExtra instructions appended to the rendered Band instructions. The agent class docstring stays the system prompt

Band’s tools are called with keyword arguments, for example await self.room.band_send_message(content="..."). A custom tool from additional_tools takes its original Pydantic validation payload as one argument, await self.room.<name>(arguments={...}), keyed by each field’s validation alias. A failed call raises BandToolError with the reason.

NooaRoomAgent subclasses nooa.Agent, exposes room: RoomTools, and declares async def respond(self, message: str) -> None, the ellipsis-bodied method the model completes for each room message. The model is set on the class, not the config, for example class NoteTaker(NooaRoomAgent, llm=get_llm_client("anthropic/claude-sonnet-5-5")); NOOA routes models through LiteLLM and reads that provider’s key from the environment.

from nooa.unifiedllm.registry import get_llm_client
from band import Emit
from band.adapters import NooaAdapter, NooaRoomAgent
class NoteTaker(NooaRoomAgent, llm=get_llm_client("anthropic/claude-sonnet-5-5")):
"""You are NoteTaker, a concise assistant that keeps notes for this room."""
notes: list[str]
def __init__(self) -> None:
super().__init__()
self.notes = []
def remember(self, note: str) -> int:
"""Store a note for this room and return how many notes there are."""
self.notes.append(note)
return len(self.notes)
adapter = NooaAdapter(agent=NoteTaker, emit=Emit.TOOL_CALLS)

Emits TOOL_CALLS, THOUGHTS, and USAGE; THOUGHTS posts each model call’s raw reasoning_content, so narrow emit to keep reasoning out of the room. Install with band-sdk[nooa], which requires Python 3.12 or 3.13 on Linux or macOS. See NOOA for a worked example.

The model writes and runs Python inside the agent process. Run a NOOA agent in a container or VM, never on a machine whose files or credentials matter.

LettaAdapter

Adapter for Letta agents with persistent memory.

from band.adapters import LettaAdapter, LettaAdapterConfig
from band.adapters.letta import LettaMCPConfig
adapter = LettaAdapter(
config: LettaAdapterConfig | None = None,
*,
history_converter: LettaHistoryConverter | None = None,
**features: Unpack[FeatureKwargs],
)
ModeBehavior
per_room (default)Each room gets its own Letta agent with isolated memory
sharedOne Letta agent serves all rooms, isolated per room through the Conversations API

LettaAdapterConfig is a pydantic-settings model: every field can also be set through a LETTA_-prefixed environment variable, and a constructor argument always wins. provider_key also reads LETTA_API_KEY.

LettaAdapterConfig fieldTypeDefaultDescription
provider_keystr | NoneNoneLetta API key. Required for Letta Cloud, optional for self-hosted Letta
base_urlstr"https://api.letta.com"Letta server URL; set it to a self-hosted server such as http://localhost:8283
projectstr | NoneNoneLetta Cloud project; ignored for self-hosted
mode"per_room" | "shared""per_room"See the mode table
agent_idstr | NoneNoneExisting Letta agent to resume; in shared mode, a bootstrap hint for the lone agent
modelstr | NoneNoneLetta model handle with its provider prefix, such as "openai/gpt-5.4-mini"; sent only when set
embeddingstr | NoneNoneEmbedding model used when creating an agent. Letta’s Docker server requires it; Cloud picks its own default
custom_sectionstr""Additional instructions added to the system prompt
include_base_instructionsboolTrueInclude the Band base instructions in the system prompt
personastr | NoneNoneFull override of the created agent’s persona block
memory_blockstuple[dict[str, str], ...]()Additional memory blocks for created agents
summary_max_lengthint150Maximum characters of the per-turn summary
mcpLettaMCPConfigLettaMCPConfig()How the Letta server reaches Band’s tools; see below
org_scopedbool | NoneNoneSelf-hosted only: provision a dedicated Letta organization and user per adapter instance so MCP tool registrations never collide between instances on one server. None enables it for a self-hosted base_url and leaves it off for Letta Cloud; True against Letta Cloud raises BandConfigError
auto_relayboolTrueRelay the agent’s plain assistant text into the room when it did not call the MCP send tool. False drops the text, so a turn that neither replied nor did work is reported as a missing reply
turn_timeout_sfloat300.0Seconds a Letta round trip may take before the turn fails with Letta agent response timed out after <n>s
delete_agents_on_cleanupboolFalseDelete the room’s Letta agent on cleanup; per_room mode only. Off by default so resume-by-ID works across restarts
consolidate_memory_on_cleanupboolTrueAsk Letta to consolidate room context into memory on cleanup
teardown_timeout_sfloat10.0Upper bound for best-effort Letta teardown calls
LettaMCPConfig fieldTypeDefaultDescription
mode"self_host" | "external""self_host"self_host starts an in-process Band MCP server and registers it with Letta; external registers a separately run Band MCP server, which Letta Cloud requires
server_urlstr"http://localhost:8002/sse"external mode: URL of the running Band MCP server
server_namestr | NoneNoneRegistration name in Letta. None is "band" in external mode and a fresh band-<suffix> per registration when self-hosted
bind_hoststr"127.0.0.1"self_host mode: interface the local server binds. A non-loopback bind exposes the agent’s tools to the network
advertised_hoststr | NoneNoneself_host mode: hostname Letta uses to reach the local server; "host.docker.internal" for a dockerized Letta
transport"sse" | "streamable_http""sse"MCP transport Letta uses

api_key is a deprecated constructor alias for provider_key that emits DeprecationWarning: api_key is deprecated on LettaAdapterConfig, use provider_key instead; passing both raises BandConfigError: Cannot pass both provider_key and api_key. mcp_server_url and mcp_server_name are deprecated fields folded into mcp with mode="external". Emits TOOL_CALLS, TASK_EVENTS, and USAGE.

Emit.TASK_EVENTS carries the room’s Letta agent_id, which the adapter reads back to resume the server-side agent. Narrowing emit to exclude it stops resumption, so every restart creates a new Letta agent instead of reattaching.

See Letta for a worked example.

ParlantAdapter

Adapter for the Parlant behavioral engine. By default ParlantAdapter owns the Parlant server: it reserves ports and boots p.Server when the Band agent starts, and tears it down when the agent stops.

from band.adapters import ParlantAdapter, ParlantAdapterConfig
adapter = ParlantAdapter(
config: ParlantAdapterConfig | None = None,
*,
history_converter: ParlantHistoryConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
nlp_service: Any | None = None,
server_options: dict[str, Any] | None = None,
server: p.Server | None = None,
parlant_agent: p.Agent | None = None,
configure: ConfigureCallback | None = None,
**features: Unpack[FeatureKwargs],
)
ParlantAdapterConfig fieldTypeDefaultDescription
namestr | NoneNoneParlant agent name; defaults to the Band agent’s name
descriptionstr | NoneNoneParlant agent description, its behavioral instructions; defaults to the Band agent’s description
system_promptstr | NoneNoneFull override of the created Parlant agent’s description
custom_sectionstr | NoneNoneExtra instructions appended to the created agent’s description. Ignored when system_prompt is set
response_timeoutfloat300.0Total seconds allowed for the Parlant response to one turn; must be greater than 0
response_pollfloat30.0Length of each polling window inside that budget; must be greater than 0
ArgumentTypeDescription
additional_toolslist[CustomToolDef] | None(InputModel, handler) pairs attached, with Band’s tools, to every guideline that does not name its own tools
nlp_serviceAny | NoneNLP service for the adapter-owned server, such as p.NLPServices.openai
server_optionsdict[str, Any] | NoneExtra keyword arguments passed to p.Server(...); port and tool_service_port default to reserved free ports
serverp.Server | NoneYour own running server; the adapter borrows it and never tears it down
parlant_agentp.Agent | NoneYour own Parlant agent; requires server
configureConfigureCallback | NoneAsync callback run at startup with the live (server, parlant_agent)

Declare guidelines before startup with adapter.add_guideline(condition=..., action=..., tools=...), which mirrors parlant.sdk.Agent.create_guideline and attaches the adapter’s tools when tools is omitted. Parlant calls a tool only through a matched guideline or journey. Calling add_guideline() after the agent starts raises RuntimeError: add_guideline must be called before the agent starts; use the configure= callback or adapter.parlant_agent.create_guideline() for a running agent.

These combinations raise ValueError at construction: parlant_agent without server (parlant_agent requires the server it lives on; pass server= as well); server together with nlp_service or a non-empty server_options; and parlant_agent together with system_prompt or custom_section. Emits nothing; Capability.MEMORY is accepted but there are no Parlant memory tools. See Parlant for a worked example.