ag2-network-discussion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAG2 Network — Discussion Adapter
AG2网络 — Discussion适配器
discussionvalidate_sendcan_sendPrerequisite: readfirst. This skill assumes you already knowag2-network-quickstart,Hub.open, the channel lifecycle, andHubClient.registerbasics.Envelope
discussionvalidate_sendcan_send前置要求:请先阅读。本技能假设你已了解ag2-network-quickstart、Hub.open、通道生命周期和HubClient.register基础知识。Envelope
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 for:
discussion- Conditional handoffs ("if alice mentions security, hand to the security expert") → use .
ag2-network-workflow - Pipelines where each step happens once → use with
ag2-network-workflow.TransitionGraph.sequence([...]) - Two participants with no fixed order → use (covered in
conversation).ag2-network-quickstart - Strict 1Q1R → use (covered in
consulting).ag2-network-quickstart
- “三个Agent依次辩论”
- “固定成员参与的小组讨论/头脑风暴”
- “循环轮询评审员对草稿发表意见”
- 任何需要固定顺序、每个参与者每轮都有发言机会的N方轮流场景
请勿将用于:
discussion- 条件式交接(“如果Alice提到安全问题,转交安全专家”)→ 使用。
ag2-network-workflow - 每个步骤仅执行一次的流水线→ 使用配合
ag2-network-workflow。TransitionGraph.sequence([...]) - 无固定顺序的两方交流→ 使用(详见
conversation)。ag2-network-quickstart - 严格的一问一答场景→ 使用(详见
consulting)。ag2-network-quickstart
Shape
核心参数
| Participants | 2+ |
| Turn order | Round-robin (creator first, then participants in registration order) |
| Auto-close | No |
| Termination | Explicit |
| Default view | |
| Default expectations | |
| Knob | |
The view recent-window is sized to 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.
N*2| 参与者数量 | 2+ |
| 轮次顺序 | 循环轮询(创建者优先,随后按注册顺序排列参与者) |
| 自动关闭 | 否 |
| 终止方式 | 显式调用 |
| 默认视图 | |
| 默认预期设置 | |
| 配置项 | |
视图窗口大小设置为,以便每个Agent的投影覆盖最近约两个完整轮次——既保证回复有足够上下文,又不会随着讨论推进导致提示词过度膨胀。
N*2Discussion agents, the NetworkPlugin
, and turn-taking
NetworkPluginDiscussion Agent、NetworkPlugin
与轮次管理
NetworkPluginHubClient.register(...)NetworkPluginpeerschannelstaskscontextdelegateDiscussionAdapter.tools_for(...)[]sayEV_TEXT(reply.body)Because there's no , there's no "double-send" hazard specific to discussion. Turn-taking is enforced entirely by : a send from anyone other than is rejected with (). The 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.
sayvalidate_sendstate.expected_next_speakerProtocolErrorexpects <other> to speakcan_sendattach_plugin=FalseNetworkPlugindelegatepeerschannelstaskscontextpython
undefinedHubClient.register(...)NetworkPluginpeerschannelstaskscontextdelegateDiscussionAdapter.tools_for(...)[]sayEV_TEXT(reply.body)因为没有方法,discussion不存在“重复发送”的风险。轮次管理完全由强制执行:非发送的内容会被拒绝(提示“期望<其他参与者>发言”)。下文提到的探测功能让每个处理程序在非自身轮次时跳过LLM调用,因此正常操作下不会出现违规发送的情况。
sayvalidate_sendstate.expected_next_speakerProtocolErrorcan_sendattach_plugin=FalseNetworkPlugindelegatepeerschannelstaskscontextpython
undefinedBare discussion participant (no plugin verbs):
极简discussion参与者(无插件操作方法):
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`)".alice = await alice_hc.register(alice_agent, Passport(name="alice"), Resume(), attach_plugin=False)
以下示例为简洁起见使用`attach_plugin=False`;如果你的参与者需要`delegate`/`peers`等方法,请移除该参数。详细说明:`ag2-network-tools-and-views` → “适配器专属工具(`tools_for`)”。Smallest example
最简示例
python
import 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())The order in is significant — it becomes the rotation order after the creator. The cycle is therefore until the cap fires.
target=[bob.agent_id, carol.agent_id]alice → bob → carol → alice → bob → carol → …python
import 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?")
# 收到6个EV_TEXT消息后停止(即alice→bob→carol完整循环2次)。
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 → …How turn skipping works (the can_send
probe)
can_send轮次跳过机制(can_send
探测)
can_sendWhen alice sends her first envelope, the hub fans out to bob and carol. Both default handlers fire in parallel:
EV_TEXT- bob's handler — . The adapter says "yes, bob is
await hc.can_send(channel_id, bob.agent_id)." Handler runsexpected_next_speaker, sends bob's reply.Agent.ask - carol's handler — . The adapter says "no, expected_next_speaker is bob." Handler returns without engaging the LLM.
await hc.can_send(channel_id, carol.agent_id)
When bob's reply lands, the same fan-out repeats. Now , so carol's handler engages and bob's skips. No wasted LLM calls anywhere.
expected_next_speaker = carolIf you write a custom handler for a discussion channel, mirror this pattern — and delegate non-text envelopes to so you keep the auto-ack of (otherwise the channel sits in until and the hub closes it on you):
default_handlerEV_CHANNEL_INVITEPENDINGinvite_ack_timeoutpython
from 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 ...当Alice发送第一条消息时,中心节点会将广播给Bob和Carol。两者的默认处理程序会并行触发:
EV_TEXT- Bob的处理程序 — 调用。适配器返回“是,Bob是
await hc.can_send(channel_id, bob.agent_id)”。处理程序执行expected_next_speaker,发送Bob的回复。Agent.ask - Carol的处理程序 — 调用。适配器返回“否,expected_next_speaker是Bob”。处理程序直接返回,不调用LLM。
await hc.can_send(channel_id, carol.agent_id)
当Bob的回复到达时,重复上述广播流程。此时,因此Carol的处理程序会执行,Bob的则跳过。全程无不必要的LLM调用。
expected_next_speaker = carol如果你为discussion通道编写自定义处理程序,请遵循此模式——并且将非文本消息委托给,这样可以保留的自动确认功能(否则通道会停留在状态,直到触发,中心节点会自动关闭通道):
default_handlerEV_CHANNEL_INVITEPENDINGinvite_ack_timeoutpython
from 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) # 邀请确认及生命周期管理
return
if not await hc.can_send(envelope.channel_id, my_agent_id): # 异步操作 — 必须使用await
return # 未轮到我们 — 跳过LLM调用
# ... 读取WAL、生成视图、执行Agent.ask、发送回复 ...A HumanClient
in the round-robin
HumanClient循环轮询中的HumanClient
HumanClientA discussion participant doesn't have to be an . Register a ( — see → "") 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 (or pull with ) and, when an envelope lands, check (async — must await) — if it's the human's turn, prompt the person and ; otherwise wait. (The framework's own discussion tests do exactly this — a human gating on adapter state for its slot in the rotation.)
AgentHumanClienthc.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)Discussion参与者不一定是。注册一个(——详见 → “”),并像其他参与者一样加入轮换顺序。它没有自动响应的通知处理程序,因此需要应用程序来驱动其轮次:使用订阅(或使用拉取),当消息到达时,检查(异步操作 — 必须使用await)——如果轮到人类用户,则提示用户输入并调用;否则等待。(框架自身的discussion测试正是如此实现的——人类用户根据适配器状态等待自己的轮次。)
AgentHumanClienthc.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)State object
状态对象
python
@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]python
@dataclass(slots=True)
class DiscussionState:
participant_order: list[str]
expected_next_speaker: str
turn_count: int = 0participant_orderParticipant.orderEV_TEXT(current_index + 1) % len(participant_order)hub._adapter_states[channel_id]Validation rules
验证规则
DiscussionAdapter.validate_send- from anyone other than
EV_TEXT— raisesstate.expected_next_speaker.ProtocolError - Sends from non-participants.
- Sends to a closed channel.
Protocol envelopes (, ) bypass the turn check — they're bookkeeping, not turn-taking.
EV_CHANNEL_*ag2.task.*DiscussionAdapter.validate_send- 非发送的
state.expected_next_speaker——抛出EV_TEXT。ProtocolError - 非参与者发送的内容。
- 向已关闭的通道发送内容。
协议消息(、)不受轮次检查限制——它们属于管理类消息,不参与轮次交互。
EV_CHANNEL_*ag2.task.*Customising the ordering
自定义排序方式
Today only ships:
ORDERING_ROUND_ROBINpython
from 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"
)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 — gives you the same shape with the option to add conditions later. See .
workflowTransitionGraph.round_robin(participants, max_turns=N)ag2-network-workflow目前仅支持:
ORDERING_ROUND_ROBINpython
from ag2.network import ORDERING_ROUND_ROBIN
channel = await alice.open(
type="discussion",
target=[bob.agent_id, carol.agent_id],
knobs={"ordering": ORDERING_ROUND_ROBIN}, # 等同于"round_robin"
)传入其他排序方式会在创建时抛出异常。未来的排序方式(动态、加权、优先级)将在此处扩展,且不会破坏现有循环轮询的兼容性。
如果你现在就需要条件式轮次管理,请切换到适配器——可提供相同的结构,且支持后续添加条件。详见。
workflowTransitionGraph.round_robin(participants, max_turns=N)ag2-network-workflowClosing
关闭通道
discussion| 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 |
discussion| 模式 | 决策主体 | 触发时机 |
|---|---|---|
应用端上限 → | 你的编排代码 | 达到轮次数量、时间限制或任何外部条件 |
Agent端工具(通过 | LLM | 某个参与者决定“讨论结束” |
| 自定义适配器子类 | 框架 | 基于内容的停止(discussion场景中很少使用) |
TTL / | 中心节点的清理程序 | 仅作为安全兜底 |
App-side cap (canonical)
应用端上限(标准方式)
python
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")The reason flows on — pick something descriptive so observers can distinguish a clean cap from a TTL or expectation violation.
EV_CHANNEL_CLOSED.event_data["reason"]python
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")关闭原因会通过传递——请选择描述性的原因,以便观察者区分正常上限关闭与TTL或预期违规关闭。
EV_CHANNEL_CLOSED.event_data["reason"]Agent-side tool
Agent端工具
Same pattern as the other adapters — inject the active , call :
Channelclose()python
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)Any participant can wrap up. See 's "Five close routes" for the full picture.
ag2-network-quickstart与其他适配器模式相同——注入活动,调用:
Channelclose()python
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-quickstartWhen discussion
isn't enough
discussion当discussion
不足以满足需求时
discussionThe moment you need conditional turn-taking ("if bob mentions deadlines, jump to deadlines_expert"), is the wrong adapter. Migrate to — same N-party shape, but turn-taking lives in a with , , and conditions. See for the migration recipe.
discussionworkflowTransitionGraphFromSpeakerToolCalledContextEqualsag2-network-workflow一旦你需要条件式轮次管理(“如果Bob提到截止日期,切换到deadlines_expert”),就不再是合适的适配器。请迁移到——它支持相同的N方结构,但轮次管理基于,可配合、和等条件。详见中的迁移指南。
discussionworkflowTransitionGraphFromSpeakerToolCalledContextEqualsag2-network-workflowDefault expectations
默认预期设置
| 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. |
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 via .
auto_closeag2-network-governance| 预期规则 | 阈值 | 处理方式 | 作用 |
|---|---|---|---|
| 120秒 | | 记录 |
| 600秒 | | 在视图投影中将发言缓慢的参与者标记为“跳过”一个轮次。通道保持开启。 |
Discussion特意不通过预期设置自动关闭通道——发言缓慢的参与者不应该导致整个讨论终止。如果你希望通道在轮次停滞时终止,请通过将预期设置覆盖为。
ag2-network-governanceauto_closeQuick reference — imports
快速参考 — 导入
python
from 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(...)
)The string forms and work too — the constants are equivalent and exist for type-safety in larger codebases.
type="discussion"ordering="round_robin"python
from ag2.network import (
DISCUSSION_TYPE, # = "discussion"
ORDERING_ROUND_ROBIN, # = "round_robin"
DiscussionAdapter, # 适配器类本身(很少直接实例化)
DiscussionState, # 状态数据类
HumanClient, # 循环轮询中的非LLM参与者 — HubClient.register_human(...)
)字符串形式的和同样有效——常量的作用是在大型代码库中保证类型安全。
type="discussion"ordering="round_robin"