# Discussion

discussion is an N-party round-robin channel. Participants speak in a fixed order, cycling indefinitely until you close it. The adapter enforces "wait your turn" via `validate_send`; the hub's `can_send` probe lets the default handler skip wasted LLM calls when it isn't this agent's turn.

## Shape

|  |  |
| --- | --- |
| Participants | 2+ |
| Turn order | Round-robin (creator first, then participants in order) |
| Auto-close | No |
| Termination | Explicit `channel.close()` or TTL |
| Default view | `WindowedSummary(recent_n=N*2)` (where `N` = participant count) |
| Default expectations | `turn_within(120s, warn)`, `turn_within(600s, hide)` |
| Knob | `{"ordering": "round_robin"}` (only ordering shipped today) |

## Lifecycle

carolbobHub + DiscussionAdapteralicecarolbobHub + DiscussionAdapteraliceexpected_next_speaker = alicestate.expected_next_speaker ← bobstate.expected_next_speaker ← carolstate.expected_next_speaker ← alice (cycle)...continues until close() or TTLopen(type="discussion", target=[bob, carol], knobs=round_robin)EV_CHANNEL_INVITEEV_CHANNEL_INVITEEV_CHANNEL_INVITE_ACKEV_CHANNEL_INVITE_ACKEV_CHANNEL_OPENEDEV_TEXT (alice 1)deliverdeliver (probes can_send → false, no LLM)EV_TEXT (bob 1)EV_TEXT (carol 1)channel.close()EV_CHANNEL_CLOSEDEV_CHANNEL_CLOSED

The `can_send` probe lets each default handler skip its LLM call when it's not that participant's turn — see "How Turn Skipping Works" below.

## Smallest Example

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import AnthropicConfig<br>from autogen.beta.knowledge import MemoryKnowledgeStore<br>from autogen.beta.network import (<br>    EV_TEXT,<br>    ORDERING_ROUND_ROBIN,<br>    Hub,<br>)<br>config = AnthropicConfig(model="claude-sonnet-4-6")<br>hub = await Hub.open(MemoryKnowledgeStore(), ttl_sweep_interval=0)<br>alice = await hub.register(<br>    Agent("alice", prompt="The optimist. One short sentence.", config=config),<br>)<br>bob = await hub.register(<br>    Agent("bob", prompt="The realist. One short sentence.", config=config),<br>)<br>carol = await hub.register(<br>    Agent("carol", prompt="The skeptic. One short sentence.", config=config),<br>)<br>channel = await alice.open(<br>    type="discussion",<br>    target=[bob.agent_id, carol.agent_id],<br>    knobs={"ordering": ORDERING_ROUND_ROBIN},<br>)<br>await channel.send("Topic: should every developer learn Rust?")<br># After the kickoff, each agent's default handler responds when can_send<br># returns true for them — bob, then carol, then alice again, and so on.<br>``` |

To halt, cap on text count and call `channel.close()`:

|     |     |
| --- | --- |
| ```<br>1<br>2<br>``` | ```<br>await wait_for_text_count(hub, channel.channel_id, expected=6)<br>await channel.close()<br>``` |

## How Turn Skipping Works

When alice sends "alice 1", the hub fans out an `EV_TEXT` to bob and carol. Both default handlers fire in parallel:

- **bob's handler** — calls `hc.can_send(channel_id, bob.agent_id)`. The adapter says "yes, bob is `expected_next_speaker`." Handler runs `Agent.ask`, sends bob's reply.
- **carol's handler** — calls `hc.can_send(channel_id, carol.agent_id)`. The adapter says "no, expected_next_speaker is bob, not carol." Handler returns without engaging the LLM.

When bob's reply lands, the same fan-out happens. Now `expected_next_speaker = carol`, so carol's handler engages and bob's skips. No wasted LLM calls.

## When to Use

- Brainstorms with a fixed cast — three agents debating a topic in turn.
- Panel discussions where each agent has a static viewpoint.
- Round-robin reviewers — three reviewers each commenting once per cycle on a draft.

## When NOT to Use

- Conditional handoffs ("if alice mentions security, hand to the security expert") — use [workflow](https://docs.ag2.ai/latest/docs/beta/network/workflow/).
- Two participants only with no order — use [conversation](https://docs.ag2.ai/latest/docs/beta/network/conversation/).
- A pipeline where each step happens once — use [workflow](https://docs.ag2.ai/latest/docs/beta/network/workflow/) with `TransitionGraph.sequence(...)`.

## Validation Rules

`DiscussionAdapter.validate_send` rejects:

- `EV_TEXT` from anyone other than `state.expected_next_speaker`.
- Sends from non-participants.
- Sends to a closed channel.

Protocol envelopes (`EV_CHANNEL_*`, `ag2.task.*`) bypass the turn check.

## State Object

```
@dataclass(slots=True)
class DiscussionState:
    participant_order: list[str]
    expected_next_speaker: str
    turn_count: int = 0
```

Read via `hub._adapter_states[channel_id]`. The order is fixed at create time by sorting participants on `Participant.order`; round-robin advances by `(current_index + 1) % len(participant_order)`.

## Customising the Ordering

Today only `ORDERING_ROUND_ROBIN` ships. The knob is `knobs={"ordering": "round_robin"}`; passing anything else raises at create time. Future orderings (dynamic, weighted) will plug in here without breaking the round-robin contract.

## Closing

`discussion` never auto-closes. The example below caps at 6 turns and calls `channel.close()`, but four other patterns work for `discussion` too:

- **App-side cap** — count turns and call `channel.close()` (canonical, simplest).
- **Agent-side tool** — any participant calls a tool that closes the channel. See [Closing Channels → Agent-side tool](https://docs.ag2.ai/latest/docs/beta/network/termination/#pattern-2--agent-side-tool).
- **Custom adapter** — subclass `DiscussionAdapter` to fold `turn_count` and emit `CLOSING` at a cap (or switch to `workflow` with `TransitionGraph.round_robin(max_turns=N)`).
- **TTL / expectations** — safety nets only.

See [Closing Channels](https://docs.ag2.ai/latest/docs/beta/network/termination/) for the full picture.
