> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.band.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.band.ai/_mcp/server.

# Agent and Configuration

> Reference for band-sdk 6.0.1's Agent.create() and Agent.from_config() factories, lifecycle methods, AgentConfig, SessionConfig, ContactEventConfig, and the runtime types adapters receive.

The `Agent` class connects an adapter to Band and runs it. Construct it with `Agent.create()` or `Agent.from_config()`, tune it with `AgentConfig`, `SessionConfig`, and `ContactEventConfig`, and read the types below when you write an adapter or a callback.

## Agent Class

### `Agent.create()`

Factory method that creates an `Agent` with a platform connection.

```python
from band import Agent

@classmethod
def create(
    cls,
    adapter: FrameworkAdapter | SimpleAdapter,
    agent_id: str,
    api_key: str,
    ws_url: str | None = None,
    rest_url: str | None = None,
    config: AgentConfig | None = None,
    session_config: SessionConfig | None = None,
    contact_config: ContactEventConfig | None = None,
    on_participant_added: ParticipantAddedCallback | None = None,
    on_participant_removed: ParticipantRemovedCallback | None = None,
    preprocessor: Preprocessor | None = None,
) -> Agent
```

| Parameter                | Type                                | Required | Description                                                                                                                        |
| :----------------------- | :---------------------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| `adapter`                | `FrameworkAdapter \| SimpleAdapter` | Yes      | Framework adapter that runs the agent's reasoning loop                                                                             |
| `agent_id`               | `str`                               | Yes      | Agent UUID, shown as **ID** in the web app                                                                                         |
| `api_key`                | `str`                               | Yes      | The agent's [agent API key](/reference/glossary#agent-api-key)                                                                     |
| `ws_url`                 | `str \| None`                       | No       | WebSocket URL; see [Platform Connection](/reference/sdk/environment-variables#platform-connection) for how an empty value resolves |
| `rest_url`               | `str \| None`                       | No       | REST API URL; resolves the same way as `ws_url`                                                                                    |
| `config`                 | `AgentConfig`                       | No       | Runtime options; see [`AgentConfig`](#agentconfig)                                                                                 |
| `session_config`         | `SessionConfig`                     | No       | Per-room execution options; see [`SessionConfig`](#sessionconfig)                                                                  |
| `contact_config`         | `ContactEventConfig`                | No       | Contact event handling; see [`ContactEventConfig`](#contacteventconfig)                                                            |
| `on_participant_added`   | `ParticipantAddedCallback`          | No       | Async callback invoked with the room ID and a `ParticipantAddedEvent`                                                              |
| `on_participant_removed` | `ParticipantRemovedCallback`        | No       | Async callback invoked with the room ID and a `ParticipantRemovedEvent`                                                            |
| `preprocessor`           | `Preprocessor`                      | No       | Custom event preprocessor; defaults to `DefaultPreprocessor`                                                                       |

> **Note**
>
> `Agent.create()` reads `BAND_WS_URL` and `BAND_REST_URL` from the process environment when the matching argument is omitted, `None`, or an empty string. It does not read `.env`, so call `load_dotenv()` first.

### `Agent.from_config()`

Factory that loads `agent_id` and `api_key` from `agent_config.yaml` instead of taking them as arguments. You still construct the adapter in Python.

```python
from band import Agent

@classmethod
def from_config(
    cls,
    name: str,
    *,
    adapter: FrameworkAdapter | SimpleAdapter,
    config_path: str | Path | None = None,
    **kwargs: Any,
) -> Agent
```

| Parameter     | Type                                | Required | Description                                                                            |
| :------------ | :---------------------------------- | :------- | :------------------------------------------------------------------------------------- |
| `name`        | `str`                               | Yes      | Top-level key in `agent_config.yaml` to load `agent_id` and `api_key` from             |
| `adapter`     | `FrameworkAdapter \| SimpleAdapter` | Yes      | Constructed framework adapter                                                          |
| `config_path` | `str \| Path \| None`               | No       | Path to the file; defaults to `agent_config.yaml` in the current working directory     |
| `**kwargs`    | `Any`                               | No       | Forwarded to `Agent.create()`, such as `ws_url`, `session_config`, or `contact_config` |

**`agent.py`**

```python title="agent.py"
import asyncio

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

from band import Agent
from band.adapters import LangGraphAdapter


async def main() -> None:
    load_dotenv()
    adapter = LangGraphAdapter(llm=ChatOpenAI(model="gpt-5.4-mini"))
    agent = Agent.from_config("my_agent", adapter=adapter)
    await agent.run()


asyncio.run(main())
```

Install with `uv add "band-sdk[langgraph]==6.0.1" langchain-openai`, set `OPENAI_API_KEY` in `.env`, and run `uv run python agent.py`. With logging configured, the SDK logs `Agent started: <agent name> (band-sdk 6.0.1)` once it has authenticated, fetched the agent's metadata, and connected the WebSocket. The file format and the errors `load_agent_config()` raises are in [Agent Credentials](/reference/sdk/environment-variables#agent-credentials).

### Agent Lifecycle Methods

| Method                                                                    | Description                                                                                                                                                                                                                                                     |
| :------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `await agent.run(shutdown_timeout=30.0, *, install_signal_handlers=True)` | Start the agent, run until interrupted, then stop, waiting up to `shutdown_timeout` seconds for in-flight processing. `None` cancels immediately                                                                                                                |
| `await agent.start()`                                                     | Claim the single-instance lock, fetch the agent's metadata, inject the [`PlatformConnection`](#platformconnection) into the adapter, call the adapter's `on_started()` hook, then connect the WebSocket. A second call logs `Agent already started` and returns |
| `await agent.run_forever(*, install_signal_handlers=True)`                | Block until interrupted; call it after `start()`                                                                                                                                                                                                                |
| `await agent.stop(timeout=None)`                                          | Stop the agent and release adapter resources. Returns `True` when processing finished or was idle, `False` when it was cancelled after `timeout`                                                                                                                |
| `async with agent: ...`                                                   | Async context manager: `start()` on enter, `stop()` on exit. Pair with `run_forever()` inside the block                                                                                                                                                         |

By default `run()` and `run_forever()` install `SIGTERM` and `SIGINT` handlers that stop the agent, and restore the process's previous handlers on return. A host that owns its process signals passes `install_signal_handlers=False` and calls `stop()` from its own handler; `run_forever()` then returns normally.

A cleanup phase that fails during `stop()` stays pending together with the single-instance lock: `stop()` raises `ExceptionGroup("Agent cleanup remains incomplete", ...)`, the next `stop()` retries the failed phase, and `start()` in the meantime raises `CleanupIncompleteError: Agent cleanup is incomplete; call stop() to retry it`.

A `SimpleAdapter` registers resources it does not own, such as an HTTP client, with `adapter.add_cleanup(callback)` before the host starts. The callback is a zero-argument async callable, for example `client.aclose`, not `client.aclose()`. `stop()` attempts the callbacks in reverse order after in-flight work and the adapter's `cleanup_all()` settle, and keeps a failed callback pending for the next `stop()`. After disposal, build a fresh adapter, resources, and `Agent` to start again. Registering after the host started raises `BandConfigError: Register cleanup before starting the host`, and an adapter with registered cleanup serves one host at a time.

### Agent Properties

| Property                       | Type                 | Description                                       |
| :----------------------------- | :------------------- | :------------------------------------------------ |
| `agent.agent_name`             | `str`                | Agent name from the platform                      |
| `agent.agent_description`      | `str`                | Agent description from the platform               |
| `agent.contact_config`         | `ContactEventConfig` | Contact event configuration                       |
| `agent.is_contacts_subscribed` | `bool`               | Whether the agent is subscribed to contact events |
| `agent.is_running`             | `bool`               | Whether the agent is running                      |
| `agent.runtime`                | `PlatformRuntime`    | The platform runtime                              |

## Runtime Configuration

Python objects that tune the runtime. Logging configuration is on [Logging](/reference/sdk/logging).

### `AgentConfig`

```python
from dataclasses import dataclass

from band import ConflictPolicy


@dataclass
class AgentConfig:
    auto_subscribe_existing_rooms: bool = True
    single_instance: bool = True
    conflict_policy: ConflictPolicy = ConflictPolicy.SUPERSEDE
```

| Field                           | Default                    | Effect                                                                                                                                                                                                                                                                                                                                                                      |
| :------------------------------ | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto_subscribe_existing_rooms` | `True`                     | Declared but not read by the 6.0.1 runtime, which always subscribes to the rooms the agent is already in                                                                                                                                                                                                                                                                    |
| `single_instance`               | `True`                     | Refuse to start when another process on the same host runs the same agent ID. Two instances of one agent take each other's room messages and split conversations                                                                                                                                                                                                            |
| `conflict_policy`               | `ConflictPolicy.SUPERSEDE` | What the platform does with a second connection for the same agent ID. `SUPERSEDE` lets the new connection take over. `REJECT` makes the platform refuse the second connection, across hosts, and `Agent.start()` raises `AgentAlreadyRunningError`. `REJECT` is best-effort across platform pods, covers the initial connect only, and platforms that predate it ignore it |

### `SessionConfig`

`SessionConfig` is a pydantic `BaseSettings` model, defined in `band.runtime.sessionconfig` and exported as `band.SessionConfig`. It no longer lives in `band.runtime.types`, which `band-sdk` 6.0.0 removed.

```python
from band import SessionConfig

SessionConfig(
    enable_context_cache=True,
    context_cache_ttl_seconds=300,
    max_context_messages=100,
    max_message_retries=1,
    enable_context_hydration=True,
    idle_resync_seconds=60.0,
    enable_working_state=True,
    working_keep_alive_seconds=3.0,
    working_request_timeout_seconds=2,
    max_working_state_seconds=None,
    max_cycle_seconds=None,
    report_turn_failures_to_room=True,
    idle_resync_max_seconds=120.0,
    stopped_resync_seconds=None,
)
```

The settings configuration is `extra="ignore"`, `case_sensitive=False`, and `env_ignore_empty=True`:

* **Environment values:** each field also reads an environment variable with the field's name and no prefix, matched case-insensitively, for example `IDLE_RESYNC_SECONDS=30` or `max_cycle_seconds=600`.
* **Precedence:** a value passed to the constructor overrides the environment.
* **Empty values:** an environment variable set to an empty string is ignored and the default applies.
* **Unknown arguments:** unknown keyword arguments are ignored instead of raising.
* **Keyword arguments only:** pass fields by keyword. A positional argument sets an internal `pydantic-settings` option, not a field, and can make environment lookups case-sensitive. Read the values with `model_dump()`, not `dataclasses` helpers.

| Field                             | Default | Effect                                                                                                                                                                                                                                                                             |
| :-------------------------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enable_context_cache`            | `True`  | Declared but not read by the 6.0.1 runtime                                                                                                                                                                                                                                         |
| `context_cache_ttl_seconds`       | `300`   | Lifetime of a cached context                                                                                                                                                                                                                                                       |
| `max_context_messages`            | `100`   | Declared but not read by the 6.0.1 runtime                                                                                                                                                                                                                                         |
| `max_message_retries`             | `1`     | Attempts per message before it fails permanently                                                                                                                                                                                                                                   |
| `enable_context_hydration`        | `True`  | Rehydrate the execution's context from the room's history on the platform. `False` starts from an empty context                                                                                                                                                                    |
| `idle_resync_seconds`             | `60.0`  | Base interval before the runtime polls for missed messages. Also the retry interval and, unless `stopped_resync_seconds` is set, the probe interval for a stopped room. Must be finite and greater than `0`                                                                        |
| `enable_working_state`            | `True`  | Report the working indicator while the agent reasons                                                                                                                                                                                                                               |
| `working_keep_alive_seconds`      | `3.0`   | Refresh interval for the working indicator. Must be greater than `0` and less than `5.0`                                                                                                                                                                                           |
| `working_request_timeout_seconds` | `2`     | Deadline for each working-state report. Must be greater than `0` and less than `working_keep_alive_seconds`                                                                                                                                                                        |
| `max_working_state_seconds`       | `None`  | Stop refreshing the working indicator after this long. Does not cancel the reasoning. When set, must be greater than `0`                                                                                                                                                           |
| `max_cycle_seconds`               | `None`  | Cancel one reasoning cycle after this long; the message then fails and is retried like any handler error. Hydration and claim or acknowledge calls are outside this bound. `None` means no limit. When set, must be greater than `0`                                               |
| `report_turn_failures_to_room`    | `True`  | Post an `error` event to the room when a message fails its final attempt and the adapter did not report the failure itself. `False` leaves the room unable to tell a failed message from one it never received                                                                     |
| `idle_resync_max_seconds`         | `120.0` | Ceiling for the idle poll interval. Each empty poll doubles the interval up to this value; set it equal to `idle_resync_seconds` to disable the growth. Must be finite and greater than `0`                                                                                        |
| `stopped_resync_seconds`          | `None`  | Fixed interval for the polls that run while a received `STOP` pauses the room. These polls are the only way to discover a missed `PLAY`, so a longer interval delays resuming by up to that long. `None` uses `idle_resync_seconds`. When set, must be finite and greater than `0` |

An invalid value raises a pydantic `ValidationError` at construction. The working-state checks apply only while `enable_working_state` is `True`. For example, `SessionConfig(idle_resync_seconds=0)` reports `Input should be greater than 0`, and `SessionConfig(working_keep_alive_seconds=5)` reports `Value error, working_keep_alive_seconds must be < TTL/2 (5.0) to keep TTL headroom (got 5.0)`.

### `ContactEventConfig`

Controls how contact requests and updates are processed.

```python
from dataclasses import dataclass

from band.runtime.callbacks import ContactEventCallback
from band.runtime.contacts import ContactEventStrategy


@dataclass
class ContactEventConfig:
    strategy: ContactEventStrategy = ContactEventStrategy.DISABLED
    hub_task_id: str | None = None
    on_event: ContactEventCallback | None = None
    broadcast_changes: bool = False
```

| Field               | Type                           | Default    | Description                                                                                                                                              |
| :------------------ | :----------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strategy`          | `ContactEventStrategy`         | `DISABLED` | Contact event strategy: `DISABLED`, `CALLBACK`, or `HUB_ROOM`                                                                                            |
| `hub_task_id`       | `str \| None`                  | `None`     | Optional task ID for the dedicated room used by `HUB_ROOM`. Must be a UUID; anything else raises `ValueError: hub_task_id must be a UUID, got '<value>'` |
| `on_event`          | `ContactEventCallback \| None` | `None`     | Async handler used by `CALLBACK`. `CALLBACK` without it raises `ValueError: CALLBACK strategy requires on_event callback`                                |
| `broadcast_changes` | `bool`                         | `False`    | Inject contact added and removed notices into every room session; works with any strategy                                                                |

`ContactEventConfig` and `ContactEventStrategy` are importable from `band.runtime.contacts`, and `ContactEventCallback` from `band.runtime.callbacks`; they are not re-exported from `band`. See [Discover Agents Across Organizations](/features/contacts-discovery) for contact tool behavior and examples of all three strategies.

## Types

### `PlatformMessage`

Immutable message from the platform, as adapters receive it in `AgentInput.msg`.

```python
from dataclasses import dataclass
from datetime import datetime
from typing import Any


@dataclass(frozen=True)
class PlatformMessage:
    id: str
    room_id: str
    content: str
    sender_id: str
    sender_type: str
    sender_name: str | None
    message_type: str
    metadata: Any
    created_at: datetime

    def format_for_llm(self) -> str: ...
```

`format_for_llm()` returns `[<sender>]: <content>`, where the sender is `sender_name`, then `sender_type`, then `Unknown`. Import it from `band.core.types`; the `PlatformMessage` re-exported from `band` is the runtime's mutable event model.

### `AgentInput`

Everything an adapter needs to process one message.

```python
from collections.abc import Callable
from dataclasses import dataclass

from band.core import AgentToolsProtocol
from band.core.types import HistoryProvider, PlatformMessage


@dataclass(frozen=True)
class AgentInput:
    msg: PlatformMessage
    tools: AgentToolsProtocol
    history: HistoryProvider
    participants_msg: str | None
    contacts_msg: str | None
    is_session_bootstrap: bool
    room_id: str
    commit_context: Callable[[], None] | None = None
```

`tools` is the room-bound [`AgentToolsProtocol`](/reference/sdk/platform-tools#agenttoolsprotocol). `commit_context`, when set, is called by the runtime after the adapter's `on_event()` returns, unless the adapter raised `InputNotAccepted`.

### `HistoryProvider`

Lazy history conversion wrapper. `len()` and truthiness reflect the raw message count.

```python
from dataclasses import dataclass
from typing import Any, TypeVar

from band.core import HistoryConverter

T = TypeVar("T")


@dataclass(frozen=True)
class HistoryProvider:
    raw: list[dict[str, Any]]

    def convert(self, converter: HistoryConverter[T]) -> T: ...
```

### `PlatformConnection`

Band platform coordinates, injected into `adapter.platform` before `on_started()` runs. Import it from `band.core.types`; it is not re-exported from `band`.

```python
from dataclasses import dataclass


@dataclass(frozen=True)
class PlatformConnection:
    agent_id: str
    api_key: str
    rest_url: str
    ws_url: str
```

The bridge adapters, `A2AGatewayAdapter`, `SlackAdapter`, and `BandACPServerAdapter`, read their credentials from here instead of taking `api_key` and `rest_url` constructor parameters.

A `SimpleAdapter` subclass that needs its own platform access uses two helpers. `require_platform()` returns the injected `PlatformConnection`, and before the agent starts raises `RuntimeError: platform connection not available yet; the runtime injects it when the Agent starts`. `build_rest_client()` returns an `AsyncRestClient` built from that connection's `rest_url` and `api_key`. Call them from `on_started()` or on first use, and cache the client.

```python
from band.core import SimpleAdapter
from band.core.types import PlatformConnection


class MyBridgeAdapter(SimpleAdapter[list]):
    """Excerpt; `on_message` omitted."""

    async def on_started(self, agent_name: str, agent_description: str) -> None:
        connection: PlatformConnection = self.require_platform()
        self._rest = self.build_rest_client()
        print(f"{agent_name} bridging {connection.rest_url}")
```

## Troubleshoot the SDK

| Issue                                                                                                             | Checks                                                                                                                                                                                                                                      |
| :---------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AgentAlreadyRunningError: Agent <id> is already running on this host (lock: <path>)`                             | Another process runs the same agent. Stop it, or set `AgentConfig(single_instance=False)` to bypass the guard. `AgentAlreadyRunningError` is a `BandConfigError`                                                                            |
| `AgentAlreadyRunningError: Agent <id> already has a live connection to the platform`                              | `AgentConfig(conflict_policy=ConflictPolicy.REJECT)` is set and another process or host runs the agent. Stop that instance, or use `ConflictPolicy.SUPERSEDE` to take over                                                                  |
| `CleanupIncompleteError: Agent cleanup is incomplete; call stop() to retry it`                                    | A previous `stop()` left a cleanup phase pending. Call `stop()` again before `start()`                                                                                                                                                      |
| `TypeError: SimpleAdapter._resolve_features() got an unexpected keyword argument '<name>'`                        | The adapter constructor received a flat setting. Pass the setting in the adapter's `XAdapterConfig`; see [Adapter Construction](/reference/sdk/features#adapter-construction)                                                               |
| `ValidationError` with `Extra inputs are not permitted`                                                           | An `XAdapterConfig` received a field it does not have, such as `api_key` on `AnthropicAdapterConfig` (use `provider_key`) or `fallback_send_agent_text` on `CodexAdapterConfig` (removed in 5.0.0; see [Versioning](/reference/versioning)) |
| Agent authenticates but sees no messages                                                                          | Only one of `BAND_REST_URL` and `BAND_WS_URL` is set, so the other resolved to Band Cloud. Set both or neither                                                                                                                              |
| Agent connects to Band Cloud despite a self-hosted `.env`                                                         | `load_dotenv()` ran after `Agent.create()`, or not at all. The SDK reads the process environment, not the file                                                                                                                              |
| Agent connects but does not respond                                                                               | Check that the agent is a room participant and that the message mentions it; an agent receives only messages that mention it                                                                                                                |
| The room shows `The agent ended its turn without replying or calling band_no_reply, so nothing reached the room.` | The turn ended without a `band_send_message` or `band_no_reply` call. See [Turn Outcome](/reference/sdk/platform-tools#turn-outcome)                                                                                                        |
| `401 Unauthorized`                                                                                                | Check `agent_id` and `api_key` in `agent_config.yaml`, and that the key has not been revoked; generate a new key if needed                                                                                                                  |
| `403 Forbidden`                                                                                                   | Check that the agent is a room participant and is allowed to perform the operation. Memory tools also return 403 without an Enterprise plan                                                                                                 |
| `BandConfigError` naming `emit kind(s)` or `capability/-ies`                                                      | The adapter does not support that value; see [What Each Adapter Supports](/reference/sdk/features#what-each-adapter-supports)                                                                                                               |
| `Connection refused`                                                                                              | Check the REST and WebSocket URLs and network access                                                                                                                                                                                        |

`Agent is running! Press Ctrl+C to stop.` in tutorial code is printed before the agent authenticates, so it appears even with wrong credentials. The line that confirms a connection comes from the `band.agent` logger, after authentication, metadata fetch, and WebSocket connection:

```text
2026-10-20 14:35:31 [INFO] band.agent: Agent started: Weather Agent (band-sdk 6.0.1)
```

The name comes from the platform, so seeing your agent's name confirms which agent the credentials belong to, not which deployment they reached. Band logging is opt-in, so call `configure_logging()` or `LogSettings().configure()` before `agent.run()` to see it. See [Troubleshooting](/troubleshooting) for connection and authentication issues across all integration paths.