
Ag2 Network Discussion
- 27 installs
- 8 repo stars
- Updated July 27, 2026
- ag2ai/ag2-skills
ag2-network-discussion is a Claude Code skill that opens an AG2 network discussion channel for fixed-order N-party round-robin turn-taking among agents.
About
This skill opens an AG2 network discussion channel, an N-party round-robin where each participant speaks in fixed order, cycling until an explicit close or TTL. A developer uses it for a brainstorm with a fixed cast, a panel discussion, or round-robin reviewers. It documents the ordering knob, the expected_next_speaker rotation, and the can_send probe pattern that lets agents skip wasted LLM calls when it is not their turn.
- Opens an N-party round-robin discussion channel on the AG2 network
- Participants speak in fixed order, cycling until explicit close or TTL
- Covers the can_send probe so agents skip LLM calls out of turn
Ag2 Network Discussion by the numbers
- 27 all-time installs (skills.sh)
- Ranked #9,601 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
ag2-network-discussion capabilities & compatibility
Free skill; requires an LLM provider API key to run the agents.
- Capabilities
- multi agent network · round robin turntaking · agent orchestration
- Use cases
- orchestration
- Pricing
- Bring your own API key
What ag2-network-discussion says it does
`discussion` is an N-party round-robin channel. Participants speak in a fixed order, cycling indefinitely until you close it.
Any fixed-order N-party turn-taking where each participant gets a slot per cycle
npx skills add https://github.com/ag2ai/ag2-skills --skill ag2-network-discussionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 8 |
| Last updated | July 27, 2026 |
| Repository | ag2ai/ag2-skills ↗ |
What it does
Run a fixed-order N-party round-robin among AG2 agents, such as a panel discussion or round-robin reviewers.
Who is it for?
Developers who want a brainstorm, panel, or round-robin reviewers with a fixed cast of AG2 agents.
Skip if: Conditional handoffs or one-shot pipelines, which use ag2-network-workflow instead.
When should I use this skill?
The user wants a brainstorm with a fixed cast, a panel discussion, or round-robin reviewers.
What you get
Agents speak in a fixed round-robin order and skip LLM calls when it is not their turn.
By the numbers
- Participants 2+
- default view NamedWindowedSummary(recent_n=N*2)
Files
AG2 Network — Discussion Adapter
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 each agent's default handler skip wasted LLM calls when it isn't that agent's turn.
Prerequisite: readag2-network-quickstartfirst. This skill assumes you already knowHub.open,HubClient.register, the channel lifecycle, andEnvelopebasics.
When to use
- "Three agents debating in turn"
- "A panel discussion / brainstorm with a fixed cast"
- "Round-robin reviewers commenting on a draft"
- Any fixed-order N-party turn-taking where each participant gets a slot per cycle
Don't use `discussion` for:
- Conditional handoffs ("if alice mentions security, hand to the security expert") → use
ag2-network-workflow. - Pipelines where each step happens once → use
ag2-network-workflowwithTransitionGraph.sequence([...]). - Two participants with no fixed order → use
conversation(covered inag2-network-quickstart). - Strict 1Q1R → use
consulting(covered inag2-network-quickstart).
Shape
| Participants | 2+ |
| Turn order | Round-robin (creator first, then participants in registration order) |
| Auto-close | No |
| Termination | Explicit channel.close() or TTL (see "Closing" below) |
| Default view | NamedWindowedSummary(recent_n=N*2) where N is the participant count |
| Default expectations | turn_within(120s, warn), turn_within(600s, hide) |
| Knob | {"ordering": ORDERING_ROUND_ROBIN} (only ordering shipped today) |
The view recent-window is sized to N*2 so each agent's projection covers roughly the last two full cycles — enough context for a coherent reply without ballooning the prompt as the discussion grows.
Discussion agents, the NetworkPlugin, and turn-taking
HubClient.register(...) attaches NetworkPlugin by default — that adds peers / channels / tasks / context / delegate, the identity-level verbs. DiscussionAdapter.tools_for(...) returns []: the discussion adapter offers no channel-specific tools (no say). A participant contributes its turn purely by returning a reply — the default handler posts the round-end EV_TEXT(reply.body) as that turn's contribution.
Because there's no say, there's no "double-send" hazard specific to discussion. Turn-taking is enforced entirely by validate_send: a send from anyone other than state.expected_next_speaker is rejected with ProtocolError (expects <other> to speak). The can_send probe (below) lets each handler skip its LLM call when it isn't its turn, so out-of-turn sends shouldn't happen in normal operation.
attach_plugin=False only removes the five NetworkPlugin verbs (delegate / peers / channels / tasks / context); it has no effect on adapter tools (discussion has none). Use it for a bare participant.
# Bare discussion participant (no plugin verbs):
alice = await alice_hc.register(alice_agent, Passport(name="alice"), Resume(), attach_plugin=False)The examples below pass attach_plugin=False for compactness; drop it if your participants need delegate / peers / etc. Full detail: ag2-network-tools-and-views → "Adapter-owned tools (tools_for)".
Smallest example
import asyncio
from autogen.beta import Agent
from autogen.beta.config import AnthropicConfig
from autogen.beta.knowledge import MemoryKnowledgeStore
from autogen.beta.network import (
EV_CHANNEL_CLOSED,
EV_TEXT,
ORDERING_ROUND_ROBIN,
Hub,
HubClient,
LocalLink,
Passport,
Resume,
)
async def main() -> None:
config = AnthropicConfig(model="claude-sonnet-4-6")
hub = await Hub.open(MemoryKnowledgeStore(), ttl_sweep_interval=0)
link = LocalLink(hub)
alice_hc, bob_hc, carol_hc = (HubClient(link, hub=hub) for _ in range(3))
alice = await alice_hc.register(
Agent("alice", prompt="The optimist. One short sentence.", config=config),
Passport(name="alice"), Resume(), attach_plugin=False,
)
bob = await bob_hc.register(
Agent("bob", prompt="The realist. One short sentence.", config=config),
Passport(name="bob"), Resume(), attach_plugin=False,
)
carol = await carol_hc.register(
Agent("carol", prompt="The skeptic. One short sentence.", config=config),
Passport(name="carol"), Resume(), attach_plugin=False,
)
channel = await alice.open(
type="discussion",
target=[bob.agent_id, carol.agent_id],
knobs={"ordering": ORDERING_ROUND_ROBIN},
)
await channel.send("Topic: should every developer learn Rust?")
# Halt after 6 EV_TEXT envelopes (= 2 full alice→bob→carol cycles).
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")
for hc in (alice_hc, bob_hc, carol_hc):
await hc.close()
await hub.close()
async def wait_for_text_count(hub, channel_id, expected, *, timeout=180.0):
deadline = asyncio.get_event_loop().time() + timeout
while asyncio.get_event_loop().time() < deadline:
wal = await hub.read_wal(channel_id)
if sum(1 for e in wal if e.event_type == EV_TEXT) >= expected:
return
await asyncio.sleep(0.05)
raise asyncio.TimeoutError("did not reach expected count")
asyncio.run(main())The order in target=[bob.agent_id, carol.agent_id] is significant — it becomes the rotation order after the creator. The cycle is therefore alice → bob → carol → alice → bob → carol → … until the cap fires.
How turn skipping works (the can_send probe)
When alice sends her first envelope, the hub fans EV_TEXT out to bob and carol. Both default handlers fire in parallel:
- bob's handler —
await hc.can_send(channel_id, bob.agent_id). The adapter says "yes, bob isexpected_next_speaker." Handler runsAgent.ask, sends bob's reply. - carol's handler —
await hc.can_send(channel_id, carol.agent_id). The adapter says "no, expected_next_speaker is bob." Handler returns without engaging the LLM.
When bob's reply lands, the same fan-out repeats. Now expected_next_speaker = carol, so carol's handler engages and bob's skips. No wasted LLM calls anywhere.
If you write a custom handler for a discussion channel, mirror this pattern — and delegate non-text envelopes to `default_handler` so you keep the auto-ack of EV_CHANNEL_INVITE (otherwise the channel sits in PENDING until invite_ack_timeout and the hub closes it on you):
from autogen.beta.network import Envelope, EV_TEXT, default_handler
async def my_handler(envelope: Envelope) -> None:
if envelope.event_type != EV_TEXT:
await default_handler(envelope, client) # invite-ack + lifecycle bookkeeping
return
if not await hc.can_send(envelope.channel_id, my_agent_id): # async — must await
return # not our turn — skip the LLM call
# ... read WAL, project view, run Agent.ask, send reply ...A HumanClient in the round-robin
A discussion participant doesn't have to be an Agent. Register a HumanClient (hc.register_human(Passport(name="user", kind="human")) — see ag2-network-quickstart → "HumanClient") and add it to the participant order like any other. It has no notify handler that auto-responds, so the application drives its turn: subscribe with on_envelope(...) (or pull with next_envelope(...)) and, when an envelope lands, check await hc.can_send(channel_id, user.agent_id) (async — must await) — if it's the human's turn, prompt the person and await user.send(channel_id, text); otherwise wait. (The framework's own discussion tests do exactly this — a human gating on adapter state for its slot in the rotation.)
State object
@dataclass(slots=True)
class DiscussionState:
participant_order: list[str]
expected_next_speaker: str
turn_count: int = 0participant_order is fixed at create time by sorting participants on Participant.order. Round-robin advances by (current_index + 1) % len(participant_order) after every accepted EV_TEXT. Read state for inspection or testing via hub._adapter_states[channel_id] (the underscore signals "operator API" — not for production agent logic).
Validation rules
DiscussionAdapter.validate_send rejects:
EV_TEXTfrom anyone other thanstate.expected_next_speaker— raisesProtocolError.- Sends from non-participants.
- Sends to a closed channel.
Protocol envelopes (EV_CHANNEL_*, ag2.task.*) bypass the turn check — they're bookkeeping, not turn-taking.
Customising the ordering
Today only ORDERING_ROUND_ROBIN ships:
from autogen.beta.network import ORDERING_ROUND_ROBIN
channel = await alice.open(
type="discussion",
target=[bob.agent_id, carol.agent_id],
knobs={"ordering": ORDERING_ROUND_ROBIN}, # equivalent to "round_robin"
)Passing any other ordering raises at create time. Future orderings (dynamic, weighted, priority) plug in here without breaking the round-robin contract.
If you need conditional turn-taking now, switch adapters to workflow — TransitionGraph.round_robin(participants, max_turns=N) gives you the same shape with the option to add conditions later. See ag2-network-workflow.
Closing
discussion never auto-closes. Four patterns work; pick by who decides:
| Pattern | Who decides | When |
|---|---|---|
App-side cap → channel.close() | Your orchestration code | Turn count, time, or any external predicate |
Agent-side tool (via ChannelInject) | The LLM | A participant decides "we're done" |
| Custom adapter subclass | The framework | Content-based stop (rare for discussions) |
TTL / turn_within expectations | The hub's sweepers | Safety net only |
App-side cap (canonical)
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")The reason flows on EV_CHANNEL_CLOSED.event_data["reason"] — pick something descriptive so observers can distinguish a clean cap from a TTL or expectation violation.
Agent-side tool
Same pattern as the other adapters — inject the active Channel, call close():
from autogen.beta.network.client.inject import ChannelInject
async def end_discussion(reason: str, channel: ChannelInject) -> str:
if channel is None:
return "no active channel"
await channel.close(reason=f"agent_close:{reason}")
return f"closed: {reason}"
for agent in (alice_agent, bob_agent, carol_agent):
agent.tool(end_discussion)Any participant can wrap up. See ag2-network-quickstart's "Five close routes" for the full picture.
When discussion isn't enough
The moment you need conditional turn-taking ("if bob mentions deadlines, jump to deadlines_expert"), discussion is the wrong adapter. Migrate to workflow — same N-party shape, but turn-taking lives in a TransitionGraph with FromSpeaker, ToolCalled, and ContextEquals conditions. See ag2-network-workflow for the migration recipe.
Default expectations
| Expectation | Threshold | Handler | What it does |
|---|---|---|---|
turn_within | 120s | warn | Logs AUDIT_KIND_EXPECTATION_VIOLATED. Channel stays open. |
turn_within | 600s | hide | Marks the slow speaker as "skip" in the view projection for one cycle. Channel stays open. |
Discussion deliberately never auto-closes from expectations — a slow speaker shouldn't kill the whole panel. If you want the channel to terminate on a stalled cycle, override the expectations to auto_close via ag2-network-governance.
Quick reference — imports
from autogen.beta.network import (
DISCUSSION_TYPE, # = "discussion"
ORDERING_ROUND_ROBIN, # = "round_robin"
DiscussionAdapter, # the adapter class itself (rarely instantiated directly)
DiscussionState, # state dataclass
HumanClient, # a non-LLM participant in the rotation — HubClient.register_human(...)
)The string forms type="discussion" and ordering="round_robin" work too — the constants are equivalent and exist for type-safety in larger codebases.
Related skills
FAQ
How many participants does a discussion channel support?
Two or more, speaking round-robin in registration order.
When should I not use discussion?
For conditional handoffs or one-shot pipelines, use ag2-network-workflow instead.