Loading...
Loading...
Open an AG2 network `discussion` channel — N-party (2+) round-robin where each participant speaks in fixed order, cycling until explicit close or TTL. Use when the user wants a brainstorm with a fixed cast, a panel discussion, or round-robin reviewers. Covers `agent_client.open(type="discussion", target=[...], knobs={"ordering": ORDERING_ROUND_ROBIN})`, the `expected_next_speaker` rotation, the `hc.can_send(...)` probe pattern (default handlers skip LLM calls when it isn't their turn), `DiscussionState`, the `turn_within` expectation defaults (`warn` at 120s / `hide` at 600s), view-window sizing for N participants, and the four close patterns that work with this adapter. Load this after `ag2-network-quickstart`. For conditional handoffs or declarative orchestration, see `ag2-network-workflow` instead.
npx skill4agent add ag2ai/ag2-skills ag2-network-discussiondiscussionvalidate_sendcan_sendPrerequisite: readfirst. This skill assumes you already knowag2-network-quickstart,Hub.open, the channel lifecycle, andHubClient.registerbasics.Envelope
discussionag2-network-workflowag2-network-workflowTransitionGraph.sequence([...])conversationag2-network-quickstartconsultingag2-network-quickstart| Participants | 2+ |
| Turn order | Round-robin (creator first, then participants in registration order) |
| Auto-close | No |
| Termination | Explicit |
| Default view | |
| Default expectations | |
| Knob | |
N*2NetworkPluginHubClient.register(...)NetworkPluginpeerschannelstaskscontextdelegateDiscussionAdapter.tools_for(...)[]sayEV_TEXT(reply.body)sayvalidate_sendstate.expected_next_speakerProtocolErrorexpects <other> to speakcan_sendattach_plugin=FalseNetworkPlugindelegatepeerschannelstaskscontext# Bare discussion participant (no plugin verbs):
alice = await alice_hc.register(alice_agent, Passport(name="alice"), Resume(), attach_plugin=False)attach_plugin=Falsedelegatepeersag2-network-tools-and-viewstools_forimport asyncio
from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.knowledge import MemoryKnowledgeStore
from ag2.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())target=[bob.agent_id, carol.agent_id]alice → bob → carol → alice → bob → carol → …can_sendEV_TEXTawait hc.can_send(channel_id, bob.agent_id)expected_next_speakerAgent.askawait hc.can_send(channel_id, carol.agent_id)expected_next_speaker = caroldefault_handlerEV_CHANNEL_INVITEPENDINGinvite_ack_timeoutfrom ag2.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 ...HumanClientAgentHumanClienthc.register_human(Passport(name="user", kind="human"))ag2-network-quickstartHumanClienton_envelope(...)next_envelope(...)await hc.can_send(channel_id, user.agent_id)await user.send(channel_id, text)@dataclass(slots=True)
class DiscussionState:
participant_order: list[str]
expected_next_speaker: str
turn_count: int = 0participant_orderParticipant.order(current_index + 1) % len(participant_order)EV_TEXThub._adapter_states[channel_id]DiscussionAdapter.validate_sendEV_TEXTstate.expected_next_speakerProtocolErrorEV_CHANNEL_*ag2.task.*ORDERING_ROUND_ROBINfrom ag2.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"
)workflowTransitionGraph.round_robin(participants, max_turns=N)ag2-network-workflowdiscussion| Pattern | Who decides | When |
|---|---|---|
App-side cap → | Your orchestration code | Turn count, time, or any external predicate |
Agent-side tool (via | The LLM | A participant decides "we're done" |
| Custom adapter subclass | The framework | Content-based stop (rare for discussions) |
TTL / | The hub's sweepers | Safety net only |
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")EV_CHANNEL_CLOSED.event_data["reason"]Channelclose()from ag2.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)ag2-network-quickstartdiscussiondiscussionworkflowTransitionGraphFromSpeakerToolCalledContextEqualsag2-network-workflow| Expectation | Threshold | Handler | What it does |
|---|---|---|---|
| 120s | | Logs |
| 600s | | Marks the slow speaker as "skip" in the view projection for one cycle. Channel stays open. |
auto_closeag2-network-governancefrom ag2.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(...)
)type="discussion"ordering="round_robin"