
This guide explains how to create a new framework adapter for the Band SDK using the composition-based architecture.
The composition pattern separates concerns:
Critical Concept: Platform tools like thenvoi_send_message are called BY THE LLM, not by your adapter. Your adapter’s job is to give tools to the LLM and let it decide when to use them.
Participant Identification: In multi-agent rooms, your LLM needs to know WHO sent each message. The platform provides sender_name in history - use your LLM’s native mechanism for identifying speakers (e.g., OpenAI’s name field) rather than embedding names in message content.
Before diving into implementation, understand what the platform provides:
history is already converted by your HistoryConverter (or raw HistoryProvider if none set)participants_msg is only set when the participant list has changed since the last messageis_session_bootstrap means “first message delivery for this room session”, not “first message ever in the room”thenvoi_send_message directly for normal responses - let the LLM decide via tool calls. Direct calls are only for emergency/fallback behavior.Each message in the raw history includes:
Multi-agent scenarios: History includes messages from ALL participants - users AND other agents. Your converter needs to handle messages from other agents appropriately (they have role: "assistant" but aren’t YOUR agent’s messages).
The real difference (one sentence):
Everything else is detail.
Concrete example, thenvoi_send_message:
Rule of thumb:
When your framework has its own tool execution loop (like LangGraph’s ReAct agent):
AgentTools to framework-specific tool formatExample: LangGraph adapter
When you need to manage the tool execution loop yourself:
tools.get_tool_schemas("openai") or tools.get_tool_schemas("anthropic")tools.execute_tool_call(name, args)Note: Some LLM APIs return
argumentsas a JSON string instead of a dict. Parse withjson.loads()if needed.
Example: Anthropic adapter
Note: Tool-result injection is provider-specific; use whatever your client expects (Anthropic uses
role=userwith content blocks; OpenAI usesrole=tool).
Events report execution status to the platform. There are two ways events get sent:
The thenvoi_send_event tool is exposed to the LLM for sharing thoughts, errors, and task progress. The LLM decides when to use it:
The LLM calls this just like any other tool:
This is already handled when you convert tools via agent_tools_to_langchain() or pass schemas via get_tool_schemas().
Your adapter calls tools.send_event() directly to report tool execution status:
These events are NOT available to the LLM - they’re for your adapter to report what’s happening during execution.
Distinguishing errors: LLM
errorevents represent reasoning failures (“I couldn’t find X”). Adaptererrorevents represent infrastructure failures (exceptions, timeouts, API errors).
When your framework emits streaming events, forward them to the platform:
When you manage the tool loop, report events as you execute:
Always wrap LLM calls and report errors:
When a tool fails, you have three choices:
What counts as recoverable? Define in your tool layer:
These should raise ToolRecoverableError. Everything else is treated as infrastructure failure.
Convert platform history to your framework’s message format:
Key Points for History Converters:
Use native name field - If your LLM supports a name field (OpenAI does), use it instead of embedding sender names in content. This gives the LLM cleaner context about who sent each message.
Sanitize names - OpenAI’s name field has pattern restrictions (no spaces, <, |, \, /, >). Sanitize with: re.sub(r'[\s<|\\/>]+', '_', name)
Handle multi-agent rooms - Messages from other agents have role: "assistant". Don’t skip all assistant messages - only skip YOUR agent’s text messages (which are redundant with tool calls). Other agents’ messages are valuable context.
Track your agent name - Store the agent name in on_started() so your converter knows which messages to skip:
The SDK provides centralized tool definitions in runtime/tools.py. Use these instead of defining your own descriptions to ensure consistent LLM behavior across all adapters.
For Pattern 2 (adapter manages tool loop):
For Pattern 1 (framework manages tools):
Why centralized?
Add to thenvoi/adapters/__init__.py:
Your adapter exposes these tools to the LLM via AgentToolsProtocol:
Tool descriptions are centralized in runtime/tools.py. Use get_tool_description(name) to get the LLM-optimized description for any tool. This ensures consistent behavior across all adapters.
Use the name field - OpenAI messages support a name field to identify participants. Use it instead of embedding names in content. Sanitize names (no spaces/special chars).
Store tool calls as-is - Just serialize the tool call object from the LLM response. The converter wraps it in an assistant message when loading.
Store tool results as-is - The OpenAI tool message format (role: tool, tool_call_id, content). Loads directly.
Include other agents’ messages - Messages from other agents (like Weather Agent) are essential context. Only skip THIS agent’s text messages (redundant with tool calls).
is_session_bootstrap - True on first message after agent starts. Load platform history here to restore context.
participants_msg - Contains participant names. Include it so the LLM uses correct @mentions.
Avoid these common mistakes when building adapters:
Use FakeAgentTools for unit testing:
For comprehensive testing patterns including LLM mocking, history testing, and integration tests, see the Testing Agents guide.
When creating a new adapter, verify it works correctly with these tests.
Before submitting a new adapter:
get_tool_schemas(), not hardcodedon_cleanup() frees any per-room statethenvoi/adapters/langgraph.py - Pattern 1 (framework manages tools)thenvoi/adapters/pydantic_ai.py - Pattern 1 (framework manages tools)thenvoi/adapters/anthropic.py - Pattern 2 (adapter manages tool loop)thenvoi/adapters/claude_sdk.py - Pattern 1 with Claude Agent SDK