Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
450fc75
feat: add consume_all_queued and max_queued_messages session settings
AlexanderZ-Band Oct 13, 2026
83462ef
feat: judge the decline rule over the whole turn
AlexanderZ-Band Oct 13, 2026
8a8fb7e
feat: add detailed terminal-write settlement results to message lifec…
AlexanderZ-Band Oct 13, 2026
40d011b
refactor: run every message attempt through one admission and settlem…
AlexanderZ-Band Oct 13, 2026
ea63878
Merge branch 'worktree-agent-afaf4cd665f1d52a5' into int-1770-sdk-opt…
AlexanderZ-Band Oct 13, 2026
bb0a90a
feat: retain every terminal intent and park refused writes
AlexanderZ-Band Oct 13, 2026
5e80289
fix: Own Cursor's provider slot with the active turn and cancel it on…
AlexanderZ-Band Oct 13, 2026
1362105
feat: add batch input contracts
AlexanderZ-Band Oct 13, 2026
795382d
Merge branch 'worktree-agent-ae316a29f96f019a9' into int-1770-sdk-opt…
AlexanderZ-Band Oct 13, 2026
c65c249
feat: select queued messages into one room-owned attempt
AlexanderZ-Band Oct 13, 2026
4f252b6
chore: require band-sdk-core 2.9.0
AlexanderZ-Band Oct 14, 2026
31cfc92
feat: add Cursor's cancellable capacity-wait line
AlexanderZ-Band Oct 14, 2026
14917b4
docs: document consuming queued messages in one turn
AlexanderZ-Band Oct 14, 2026
ec4a81e
feat: recognize controls before any processing write
AlexanderZ-Band Oct 14, 2026
775ab97
Merge branch 'worktree-agent-aeaafcf60eb1a154e' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
bc44a88
Merge branch 'worktree-agent-a465b45ab5cd49315' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
6ba7c2d
fix: keep every batch original out of bootstrap history
AlexanderZ-Band Oct 14, 2026
2c1183f
feat: route singleton and batch turns through one builder and one judge
AlexanderZ-Band Oct 14, 2026
664c1c6
feat: register room scheduler capacity requests with the adapter
AlexanderZ-Band Oct 14, 2026
1142340
Merge branch 'worktree-agent-a09769058f02c163f' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
1bd0b5b
test: keep settlement fixtures clear of the e2e agent fixture names
AlexanderZ-Band Oct 14, 2026
afa3806
feat: run LangGraph turns over one or several original messages
AlexanderZ-Band Oct 14, 2026
58c826a
feat: run Pydantic AI turns over one or several original messages
AlexanderZ-Band Oct 14, 2026
f1569d3
feat: run Agno turns over one or several original messages
AlexanderZ-Band Oct 14, 2026
7500bb3
feat: run Strands turns over one or several original messages
AlexanderZ-Band Oct 14, 2026
c4bfd62
feat: let the Gemini adapter run one turn over several originals
AlexanderZ-Band Oct 14, 2026
1cba387
feat: let the Google ADK adapter run one turn over several originals
AlexanderZ-Band Oct 14, 2026
eb1ad5f
feat: let the NOOA adapter run one turn over several originals
AlexanderZ-Band Oct 14, 2026
00113e0
feat: run OpenCode batch turns with sender-labelled inputs and read-o…
AlexanderZ-Band Oct 14, 2026
9a5d02d
feat: run CrewAI singleton and batch turns through one builder
AlexanderZ-Band Oct 14, 2026
7cfa02b
docs: record why CrewAI Flow declines batch input
AlexanderZ-Band Oct 14, 2026
e7f2a96
feat: run Parlant singleton and batch turns through one builder
AlexanderZ-Band Oct 14, 2026
b3d2e22
feat: build ACP prompts from each current input's own sender
AlexanderZ-Band Oct 14, 2026
5a8a3a8
feat: run Copilot SDK batch turns and address every distinct sender
AlexanderZ-Band Oct 14, 2026
d627526
feat: run Letta batch turns and address every distinct sender
AlexanderZ-Band Oct 14, 2026
ac69234
test: read baseline history independent of the current-input record f…
AlexanderZ-Band Oct 14, 2026
f3a5e18
Merge branch 'worktree-agent-a0bddae310a2a4e4f' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
fbad09a
Merge branch 'worktree-agent-a4b27513b2579b51e' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
a0bbf51
Merge branch 'worktree-agent-a95825593cdd3a594' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
7ffa385
Merge branch 'worktree-agent-a8c359ace9b1762c0' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
78a9921
feat: run Claude SDK batches as one turn with an approval audience
AlexanderZ-Band Oct 14, 2026
f14ee74
feat: run Codex batches as one turn with an approval audience
AlexanderZ-Band Oct 14, 2026
0ad3455
feat: expose Cursor commands as controls and share its slot across ba…
AlexanderZ-Band Oct 14, 2026
577cfb0
Merge branch 'worktree-agent-a6b66989a44729682' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
f1bb9cb
Merge branch 'worktree-agent-add2c05dc6256b692' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
6f89b31
test: add an invocation observer and a batch-input adapter selector t…
AlexanderZ-Band Oct 14, 2026
424ea97
Merge branch 'main' into int-1770-sdk-option-to-consume-all-queued-me…
AlexanderZ-Band Oct 14, 2026
fd3def3
test: add live smokes for messages that queue behind a busy turn
AlexanderZ-Band Oct 14, 2026
442e83f
refactor: simplify the queued-message batching diff
AlexanderZ-Band Oct 14, 2026
63331ad
Merge branch 'worktree-agent-a117b1b9daea7fc95' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
718d7b6
Merge branch 'worktree-agent-a9639b007e7fd91c1' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
82f3fab
docs: list batch-capable adapters and refresh the batch-era prompt
AlexanderZ-Band Oct 14, 2026
8090133
Merge branch 'int-1770-sdk-option-to-consume-all-queued-messages-in-o…
AlexanderZ-Band Oct 14, 2026
38b4661
fix: keep the batching tree green after merging main and the diff cle…
AlexanderZ-Band Oct 14, 2026
2d71ffa
test: prove failed-row exclusion through the real REST client and peer
AlexanderZ-Band Oct 14, 2026
d3b75bb
test: pin yielded page scans, idle return and paced blockage on the s…
AlexanderZ-Band Oct 14, 2026
3e1d4af
test: cover duplicate sources, held claims and singleton-mode lifecyc…
AlexanderZ-Band Oct 14, 2026
0ed1145
test: add live smokes for the model's decisions over queued inputs
AlexanderZ-Band Oct 14, 2026
e22cb6d
test: prove the runtime keeps an ambiguously refused terminal write u…
AlexanderZ-Band Oct 14, 2026
c11e2cd
test: prove the runtime parks a rejected terminal write and paces eve…
AlexanderZ-Band Oct 14, 2026
27171a5
test: give the off-mode parking room spare retries
AlexanderZ-Band Oct 14, 2026
28dac18
test: pin all-or-nothing admission order and refusal at every position
AlexanderZ-Band Oct 14, 2026
0b48c24
test: pin INTERRUPT before and after full admission
AlexanderZ-Band Oct 14, 2026
1b05e99
test: pin STOP, PLAY and shutdown at admission, turn and acknowledgement
AlexanderZ-Band Oct 14, 2026
d96b535
test: share the failed-row fixtures, wire the batch host in both mode…
AlexanderZ-Band Oct 14, 2026
9e7c76b
test: drive every terminal-write recovery entrance through a real run…
AlexanderZ-Band Oct 14, 2026
a1d142d
test: retried intents keep their kind and error, partial ACKs and lon…
AlexanderZ-Band Oct 14, 2026
6744133
test: owed writes survive room replacement and duplicates add no work
AlexanderZ-Band Oct 14, 2026
f283ac5
test: an attached failure fails every original once; a detached late …
AlexanderZ-Band Oct 14, 2026
dcccb98
test: also drive the page scan from the periodic wake and share the i…
AlexanderZ-Band Oct 14, 2026
88bd901
test: keep conformance guards green where NOOA cannot install
AlexanderZ-Band Oct 14, 2026
3232013
test: spell admission harness call sites and keep one source for call…
AlexanderZ-Band Oct 14, 2026
9cc1b28
test: drop unused delivery-kit options
AlexanderZ-Band Oct 14, 2026
657d21c
test: retire the parking room through a real room-removed event
AlexanderZ-Band Oct 14, 2026
3f18a98
test: pin that an off-mode failed turn keeps no terminal intent
AlexanderZ-Band Oct 14, 2026
cc8f839
test: name the journal and read offsets in the later-wake scan test
AlexanderZ-Band Oct 14, 2026
16ceb73
test: select the off-mode failure cell by parametrization
AlexanderZ-Band Oct 14, 2026
25fd261
test: share one table of turns that retain a terminal intent
AlexanderZ-Band Oct 14, 2026
e5cf4e1
test: pin that a parked original is skipped by its own pending intent…
AlexanderZ-Band Oct 14, 2026
ab0fac6
wip: failing proof of an off-mode /next livelock behind a parked term…
AlexanderZ-Band Oct 14, 2026
ba57f65
wip: failing proof of a scan cursor that outlives a blocked drain
AlexanderZ-Band Oct 14, 2026
6fdda8a
test: name the parking harness outcomes instead of rebuilding them pe…
AlexanderZ-Band Oct 14, 2026
813f616
wip: failing proof of a failed batch's unconfirmed write not deferrin…
AlexanderZ-Band Oct 14, 2026
72fdd9a
wip: failing proof of an off-mode room re-reading /next in a loop on …
AlexanderZ-Band Oct 14, 2026
838d0d2
test: fold attached-failure effects into one class and tighten recove…
AlexanderZ-Band Oct 14, 2026
4021c04
wip: failing proof of an unconfirmed failed write not deferring ordin…
AlexanderZ-Band Oct 14, 2026
3d8ceb6
Merge branch 'worktree-agent-ab8be93d1b7528bd2' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
0b89fdc
Merge branch 'worktree-agent-a2ec35cff92f39912' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
8358f14
Merge branch 'worktree-agent-a73195c6f2b6a2a4e' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
3afcf1b
fix: keep ordinary work deferred behind an unconfirmed failed write a…
AlexanderZ-Band Oct 14, 2026
fa83572
test: observe the turns of a bespoke baseline agent and leave out bus…
AlexanderZ-Band Oct 14, 2026
adc1cbf
test: run the approval smokes with queued-message batching on and off
AlexanderZ-Band Oct 14, 2026
ed36374
test: expect the queued-decision and batched-approval cells
AlexanderZ-Band Oct 14, 2026
f364c7d
Merge branch 'worktree-agent-a221dd126180f5365' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
17fa224
wip: failing proof of shutdown not stopping an attempt still being ad…
AlexanderZ-Band Oct 14, 2026
5ae6c6e
test: keep the new runtime fixtures clear of the e2e peer fixture name
AlexanderZ-Band Oct 14, 2026
2f522fa
Merge branch 'worktree-agent-a35d2885d195f9669' into int-1770-sdk-opt…
AlexanderZ-Band Oct 14, 2026
4376968
fix: stop an attempt that shutdown interrupts while it is still being…
AlexanderZ-Band Oct 14, 2026
692a24f
Merge remote-tracking branch 'origin/main' into int-1770-sdk-option-t…
AlexanderZ-Band Oct 14, 2026
cc65c1e
fix: bound Cursor room retirement and quiet repeated write warnings
AlexanderZ-Band Oct 14, 2026
685726b
test: cover reconnect parking, batch start wiring, unsupported hosts …
AlexanderZ-Band Oct 14, 2026
2cb263a
test: pin the jittered periodic wake in the admission package
AlexanderZ-Band Oct 14, 2026
60e24b1
Merge remote-tracking branch 'origin/int-1770-sdk-option-to-consume-a…
AlexanderZ-Band Oct 14, 2026
a937853
test: pin the resync jitter for every runtime test and run the heartb…
AlexanderZ-Band Oct 14, 2026
a2b131f
Merge remote-tracking branch 'origin/main' into int-1770-sdk-option-t…
AlexanderZ-Band Oct 14, 2026
5a6c448
fix: serve a resync requested while a recovery pass is running
AlexanderZ-Band Oct 14, 2026
685a6c6
test: drain the recovery harness for longer than the Windows loop clo…
AlexanderZ-Band Oct 14, 2026
3096ccc
Merge remote-tracking branch 'origin/main' into int-1770-sdk-option-t…
AlexanderZ-Band Oct 14, 2026
a22b651
fix: keep scanning from the first page after pruning a skipped local …
AlexanderZ-Band Oct 14, 2026
05a71a0
fix: retain a failed intent when the platform refuses its write as st…
AlexanderZ-Band Oct 14, 2026
2742f7c
fix: keep draining the queue after an INTERRUPT
AlexanderZ-Band Oct 14, 2026
c9aaff4
fix: let a settled control leave the line instead of heading it
AlexanderZ-Band Oct 14, 2026
45c2b59
fix: keep a WebSocket-only original when a STOP pauses its turn in si…
AlexanderZ-Band Oct 14, 2026
4dbebab
docs: state that an INTERRUPT keeps the drain going and a settled con…
AlexanderZ-Band Oct 14, 2026
072efe0
Merge remote-tracking branch 'origin/main' into int-1770-sdk-option-t…
AlexanderZ-Band Oct 14, 2026
88332db
docs: describe the batching ceiling as a whole number from 1 to 100
AlexanderZ-Band Oct 14, 2026
5d97b85
test: build the Cursor turn suites' commands from the shared helpers
AlexanderZ-Band Oct 14, 2026
5965897
docs: say which batch-probe imports are deferred and why
AlexanderZ-Band Oct 14, 2026
350641b
test: import the agent and room ids from their owner and reuse the sh…
AlexanderZ-Band Oct 14, 2026
757508a
test: consume a deferred input that an earlier control made a control
AlexanderZ-Band Oct 14, 2026
b93c607
refactor: narrow message events through the ordinary and synthetic pr…
AlexanderZ-Band Oct 14, 2026
82ea7f7
refactor: let the attempt own the already-processed skip of a /next m…
AlexanderZ-Band Oct 14, 2026
d5c2a31
refactor: name the selection pass that made progress
AlexanderZ-Band Oct 14, 2026
a7cfe98
refactor: log why a selection pass parks or drops input
AlexanderZ-Band Oct 14, 2026
8d120ca
refactor: log the reason a host refused an input
AlexanderZ-Band Oct 14, 2026
d547890
refactor: read every naive timestamp as UTC through one helper
AlexanderZ-Band Oct 14, 2026
92c42c8
test: pin that a naive REST timestamp orders against an aware delivery
AlexanderZ-Band Oct 14, 2026
bdfcf2b
refactor: cut bootstrap history at the turn's earliest stored original
AlexanderZ-Band Oct 14, 2026
69f7892
test: pin that a refused input runs once the host accepts
AlexanderZ-Band Oct 14, 2026
fee469c
refactor: remove the capacity-waiting adapter contract
AlexanderZ-Band Oct 14, 2026
25ecd46
test: split the deferral tests into one class per file
AlexanderZ-Band Oct 14, 2026
095e0c4
test: pin the live approval case so paging is asserted exactly
AlexanderZ-Band Oct 14, 2026
26747da
test: pin that an INTERRUPT during hydration leaves each original mar…
AlexanderZ-Band Oct 14, 2026
3db9f39
fix: keep a recovery pass running after an INTERRUPT
AlexanderZ-Band Oct 14, 2026
aa855f3
test: let the parking harness serve a host with a control handler
AlexanderZ-Band Oct 14, 2026
c3ecd64
fix: retry an unconfirmed terminal write at most once per reconciliat…
AlexanderZ-Band Oct 14, 2026
f9efa6a
test: poll the paced ack retry above the Windows clock resolution
AlexanderZ-Band Oct 14, 2026
9155c79
fix: consume a control an INTERRUPT overtakes while it hydrates
AlexanderZ-Band Oct 14, 2026
59ae005
fix: defer a live arrival's later work behind its unconfirmed termina…
AlexanderZ-Band Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@ Today's versions happen to overlap on `pydantic`, but crewai's ceilings move wit
| API introduction | [docs.band.ai/api/introduction](https://docs.band.ai/api/introduction) |
| Contacts | [docs.band.ai/core-concepts/contacts](https://docs.band.ai/core-concepts/contacts) |
| SDK changelog | [docs.band.ai/changelog/changelog/sdks](https://docs.band.ai/changelog/changelog/sdks) |
| Consuming queued messages in one turn | [docs/consume-all-queued.md](docs/consume-all-queued.md) |
| Examples | [examples/](examples/) |

---
Expand Down
40 changes: 37 additions & 3 deletions docs/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ assert adapter.config.command == ("codex-acp",)
room context owed. Fresh sessions also keep transcript replay owed; restored
sessions already hold their history and receive only Band context. Replay uses
a nonce'd boundary marker so history cannot spoof the live message, and stops
strictly before the triggering message (`messages_before`).
strictly before the turn's earliest stored original (`messages_before`).

## Upgrading from SDK 4.x to 5.x

Expand Down Expand Up @@ -198,8 +198,8 @@ failure fails that room turn visibly instead of falling back.
turn closes or is interrupted. Cursor omits the session id on its
extension requests, so each room process gets its own client profile and binds them to
that room's active turn on that process; a request from a replaced process opens no
decision. Rooms run in parallel, one turn per room; a message that arrives while its
room's turn still runs is not accepted, so the host keeps it unacknowledged and
decision. Rooms run in parallel, one turn per room ([room turns](#cursor-room-turns)); a
message that arrives while its room's turn still runs is not accepted, so the host keeps it unacknowledged and
retries it once the room is free, and a `/cursor` reply still reaches the parked
turn. Only the turn holding the room's lease answers Cursor's extension requests,
and a failed turn reports its failure before the lease passes on, once, whether
Expand All @@ -217,6 +217,40 @@ failure fails that room turn visibly instead of falling back.
first prompt. The live `backends` lane pins the Cursor CLI and passes `E2E_CURSOR_API_KEY`
as `CURSOR_API_KEY` only to its baseline step; local runs may use a stored `agent login`.

## Cursor room turns

Each room runs one Cursor provider turn at a time, and independent rooms run in
parallel. The room's accepted `CursorTurn` owns the provider task it runs in.
A batch of queued inputs (`consume_all_queued`) is one provider prompt, admitted,
cancelled and judged exactly like a single message:

- **Admission is a synchronous check-and-claim.** Ordinary input arriving while
the room's turn is still running raises `InputNotAccepted` before any provider
submission, failure notice, context commit or completion write. The room
scheduler keeps the delivery outstanding and retries at its own pace.
- **`/cursor` commands are controls.** The runtime recognizes them
(`is_control_message`, valid or not) and hands each to `try_handle_control`
alone, never inside a batch and without starting work, so a reply always
reaches a parked turn.
- **Early delivery release does not free the room.** A turn parked on a decision
releases its message (`tools.turn.detach()`) but stays the room's accepted turn
until its task ends.
- **STOP, INTERRUPT and shutdown cancel the owned task**, also after that
release, once per turn. The task's `CancelledError` path sends `session/cancel`
to Cursor. Cancellation is requested once; it is not confirmation. Room
retirement (`on_cleanup`) wakes a parked decision as declined, gives the turn
`turn_settle_timeout_s` to end on its own, then cancels it. It is bounded by
`retention.transition_timeout_s`: an overdue disposal raises
`CleanupIncompleteError` and a retry joins it.
- **The room returns only when the task has actually ended**, including a task
cancelled before it first ran. A failed task launch leaves the room free, and a
stale turn never unbinds its successor.
- **Notices address the turn's own inputs.** A decision prompt or timeout notice
mentions each distinct sender of the inputs the turn was admitted with, once,
in first-appearance order. Later messages never change that audience, and being
mentioned grants no authority to resolve a decision; `decision_authorized_senders`
does.

## Server prompt outcomes

`ACPServer.prompt` settles on the first terminal outcome for its room:
Expand Down
4 changes: 2 additions & 2 deletions docs/adapters/claude_sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ Each room's session is owned through that room's residency
(`band.core.retention`): its connect, close and recovery run independently,
so a slow CLI in one room never holds up another.

- **A turn owns the room until it settles.** The message's turn is enrolled
- **A turn owns the room until it settles.** The turn is enrolled
before it starts and keeps the room leased through its query, tool calls,
approval waits and outcome, including after `on_message` returns early for a
approval waits and outcome, including after `on_turn` returns early for a
manual approval. Leaving the room, or `cleanup_all`, closes admission at
once, gives that turn `turn_settle_timeout_s` (`TURN_SETTLE_GRACE_S` by
default) to finish on its own, then cancels and joins it, and only then
Expand Down
253 changes: 253 additions & 0 deletions docs/consume-all-queued.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Consuming queued messages in one turn

By default every delivery is its own model turn: three messages queued behind a
busy agent cost three turns. With `SessionConfig.consume_all_queued` the room
instead hands the agent the ordinary messages already waiting, oldest first, as
one turn with one outcome. It is off unless you turn it on.

## What it does

- **Best effort.** When the room is ready for its next turn it takes what is
available then. It never waits to fill a batch and has no debounce: one waiting
message is a turn of one.
- **A ceiling, not a target.** `max_queued_messages` (1 to 100, default 10) caps
how many originals one turn takes. It bounds the count, not tokens; provider
limits still apply.
- **One room, in order.** Originals keep their sender, timestamp and metadata
(`AgentInput.messages`). Rooms are never combined.
- **Boundaries end a batch.** The ceiling, a participant or other non-message
event, and a delivery REST does not list (a thread reply, which runs on its own
at its position) all end the prefix. Later arrivals never join a turn already
chosen. See [queued-message selection](websocket-events.md#queued-message-selection).
- **All or nothing admission.** Every selected original is marked processing
before the model runs. If any mark is refused, fails or is overtaken by a STOP,
nothing runs, nothing is completed and no attempt is charged; the whole
selection is retried. See [admission](turn-outcome.md#admission).
- **One model attempt and one shared outcome.** The agent is invoked once. A
reply, decline or failure applies to every original in the turn.

## Enable it

`consume_all_queued` and `max_queued_messages` are `SessionConfig` fields, passed
as `session_config=` to `Agent.create` or `Agent.from_config`. The YAML file holds
credentials only; there is no file or environment setting for session options.

```python
import asyncio
from typing import Any

from band import Agent, SessionConfig
from band.core.exceptions import BandConfigError
from band.core.simple_adapter import SimpleAdapter

config = SessionConfig(consume_all_queued=True, max_queued_messages=25)
assert (config.consume_all_queued, config.max_queued_messages) == (True, 25)
assert SessionConfig().consume_all_queued is False # off unless asked for

for invalid in (
{"max_queued_messages": 0}, # the ceiling is 1..100
{"max_queued_messages": 101},
{"consume_all_queued": True, "max_message_retries": 2}, # one attempt per batch
):
try:
SessionConfig(**invalid)
except ValueError:
pass
else:
raise AssertionError(f"{invalid} must be rejected")


class PlainAdapter(SimpleAdapter[Any]):
"""Never declared ``supports_batch_input``."""

async def on_message(self, *args: Any, **kwargs: Any) -> None: ...


agent = Agent.create(
PlainAdapter(), agent_id="agent-1", api_key="key", session_config=config
)
with asyncio.Runner() as runner:
try:
runner.run(agent.start())
except BandConfigError as error:
assert "supports_batch_input" in str(error)
else:
raise AssertionError("an adapter without batch support must be refused")
```

Enabling it is refused when it cannot work, rather than falling back silently:

| Situation | Error |
| --- | --- |
| `max_queued_messages` outside 1 to 100, a `bool`, or not a whole number | `ValueError` from `SessionConfig` |
| `max_message_retries` other than 1 | `ValueError` from `SessionConfig` |
| Adapter without `supports_batch_input`, or a custom preprocessor without `process_batch` | `BandConfigError` from `Agent.start()` |
| A custom `execution_factory`, or an execution without a batch callback | `BandConfigError` at construction |
| One-shot invocation (`OneShotInvoker`) | `BandConfigError` at construction |

One-shot keeps its one-event-per-invocation drain; it never batches.

## Supported adapters

An adapter opts in by setting `supports_batch_input = True`. A `SimpleAdapter`
overrides `on_turn(messages, ...)`, its one model-input builder, and makes
`on_message` call `on_turn((msg,), ...)`, so a singleton and a batch share one
tool loop and one judge. A bare `FrameworkAdapter` implements
`on_messages(inp: AgentInput)` itself. The runtime hands a batch to
`on_messages`, and `AgentInput` carries every original; `msg` stays the first
one, so a singleton consumer keeps working:

```python
from datetime import UTC, datetime

from band.core.types import (
AgentInput,
HistoryProvider,
PlatformMessage,
distinct_sender_ids,
)
from band.testing.fake_tools import FakeAgentTools


def message(message_id: str, sender_id: str) -> PlatformMessage:
return PlatformMessage(
id=message_id,
room_id="room-1",
content=f"@bot {message_id}",
sender_id=sender_id,
sender_type="User",
sender_name=None,
message_type="text",
metadata={},
created_at=datetime.now(UTC),
)


def agent_input(*messages: PlatformMessage) -> AgentInput:
return AgentInput(
msg=messages[0],
messages=messages,
tools=FakeAgentTools(),
history=HistoryProvider(raw=[]),
participants_msg=None,
contacts_msg=None,
is_session_bootstrap=False,
room_id="room-1",
)


batch = agent_input(
message("m1", "alice"), message("m2", "bob"), message("m3", "alice")
)
assert [m.id for m in batch.messages] == ["m1", "m2", "m3"] # arrival order
assert batch.msg is batch.messages[0] # the real first original, not a composite
assert distinct_sender_ids(batch.messages) == ("alice", "bob") # each sender once

single = AgentInput(
msg=message("m1", "alice"),
tools=FakeAgentTools(),
history=HistoryProvider(raw=[]),
participants_msg=None,
contacts_msg=None,
is_session_bootstrap=False,
room_id="room-1",
)
assert single.messages == (single.msg,) # a turn of one is the singleton case
```

These built-in adapters declare `supports_batch_input`:

| Family | Adapters |
| --- | --- |
| Model SDKs | Anthropic, Gemini, Google ADK, LangGraph, Pydantic AI, Agno, Strands, CrewAI, Parlant, NOOA, Letta |
| Coding agents | Claude SDK, Codex, Copilot SDK, OpenCode |
| ACP clients | the generic ACP client, Copilot ACP, OMP ACP, Cursor ACP |

These do not, so enabling `consume_all_queued` with one raises `BandConfigError`
at `Agent.start()`:

| Adapter | Why |
| --- | --- |
| A2A and the A2A gateway | The remote agent or the pending A2A task owns the reply; there is no model turn to share. |
| CrewAI Flow | The Flow's run id and its reply matching are keyed to one original. |
| ACP server | Each editor prompt settles on its own peer message. |
| Slack wrapper | It forwards to an inner adapter and does not yet delegate control recognition. |

## Replies and declines

The model is told to consider every current input together. It answers with
`band_send_message`; one reply may answer several inputs. It calls
`band_no_reply` only when the turn as a whole needs no answer, and that ends the
whole turn: a decline never skips a single input. An FYI does not cancel another
input's outstanding request, and a correction or cancellation is read in context.
This is the same text singleton turns use
([declining](turn-outcome.md#declining-with-band_no_reply)). It is guidance, not
a coverage check: the SDK judges that the turn replied, declined or did work, not
that each input was answered.

## Failure, retries and recovery

- **A failed batch fails every included delivery**, with one failure report and
no automatic replay. Effects the model already caused remain real.
- **Failed inputs are never retried automatically in batch mode**, including
after a restart: a delivery the platform reports `failed` for the agent is
skipped when selecting. Re-send the message to run it again. An unfinished
`processing` delivery after a crash is still picked up, so work can repeat
(at-least-once).
- **Terminal writes are retried, the model is not.** The processed or failed
write each original owes is stored when the turn ends and retried alone if the
platform does not confirm it; see
[terminal intents and settlement](turn-outcome.md#terminal-intents-and-settlement).
Intents are held in memory, so a process exit can lose them.
- **STOP and INTERRUPT.** An INTERRUPT before every original is admitted
consumes nothing; after admission it consumes them all, and the input queued
behind them runs next. STOP leaves them recoverable. With batching off,
singleton turns follow the same admission order.

## Limitations

- **Attachments are not newly hydrated.** Batching passes each original's
existing content and metadata and adds no attachment download or multimodal
input. File tools are separate and stay behind `Capability.FILES`.
- **Metadata is kept, not shown to the model.** Each original's `metadata` stays
on its own `PlatformMessage`; batching does not merge it or add it to prompts.
- **WebSocket-only originals are not durable.** A delivery REST omits is kept
only while the room's execution holds its payload. For such backlog the
stopped-room `/next` probe cannot tell an empty room from a missed PLAY; see
[limits of WebSocket-only backlog](websocket-events.md#limits-of-websocket-only-backlog).
- **Long failed history is re-read.** Failed deliveries ahead of new work are
scanned 100 per pass; nothing hides them, so a later scan can find one another
runtime reopened.
- **Cursor runs one turn per room.** A Cursor room with a running turn returns
ordinary input to the room's scheduler instead of batching it into that turn; see
[Cursor room turns](acp.md#cursor-room-turns).

## Approvals and controls

Approval, status and question replies are recognised through a read-only
`is_control_message(msg)` adapter hook and handled one at a time under their own
delivery and the existing authorization rules, never inside a batch. A reply
recognised as a control ends the batch before it. Recognition alone never
consumes or acknowledges a message, and every approval request keeps its own
identity.

A control that settles (handled, interrupted, or failed for good) leaves the line
at once. Only one that could not be attempted yet stays the deferred head, and
input behind it waits for the scheduler.

An adapter that overrides `try_handle_control` without `is_control_message`
is treated conservatively: every message is a control, so nothing batches.
Claude SDK recognises its approval commands while approvals are on, Codex
recognises every local command, and Cursor recognises `/cursor` commands, valid
or not.

## Senders and reply audience

Each original keeps its own sender ID, name and type, and the model input
labels each current input with them. An automatic reply or notice (one the SDK
sends, not one the model addresses itself) mentions each distinct original
sender once, in first-appearance order: a batch from A, B, A addresses A and B.
Adapters read that audience with `distinct_sender_ids(messages)` from
`band.core.types`.
Mentions the model chooses are never rewritten. Approval authorization comes
from the adapter's configured rules, never from the union of message senders.
Loading
Loading