ag2-network-discussion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AG2 Network — Discussion Adapter

AG2网络 — 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 each agent's default handler skip wasted LLM calls when it isn't that agent's turn.
Prerequisite: read
ag2-network-quickstart
first. This skill assumes you already know
Hub.open
,
HubClient.register
, the channel lifecycle, and
Envelope
basics.
discussion
是一种支持N方参与者的循环轮询通道。参与者按固定顺序发言,无限循环直到你关闭通道。适配器通过
validate_send
强制执行“等待轮到你”规则;中心节点的
can_send
探测功能让每个Agent的默认处理程序在非自身轮次时跳过不必要的LLM调用。
前置要求:请先阅读
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
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-workflow
    with
    TransitionGraph.sequence([...])
    .
  • Two participants with no fixed order → use
    conversation
    (covered in
    ag2-network-quickstart
    ).
  • Strict 1Q1R → use
    consulting
    (covered in
    ag2-network-quickstart
    ).
  • “三个Agent依次辩论”
  • “固定成员参与的小组讨论/头脑风暴”
  • “循环轮询评审员对草稿发表意见”
  • 任何需要固定顺序、每个参与者每轮都有发言机会的N方轮流场景
请勿将
discussion
用于:
  • 条件式交接(“如果Alice提到安全问题,转交安全专家”)→ 使用
    ag2-network-workflow
    。
  • 每个步骤仅执行一次的流水线→ 使用
    ag2-network-workflow
    配合
    TransitionGraph.sequence([...])
    。
  • 无固定顺序的两方交流→ 使用
    conversation
    (详见
    ag2-network-quickstart
    )。
  • 严格的一问一答场景→ 使用
    consulting
    (详见
    ag2-network-quickstart
    )。

Shape

核心参数

Participants2+
Turn orderRound-robin (creator first, then participants in registration order)
Auto-closeNo
TerminationExplicit
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.
参与者数量2+
轮次顺序循环轮询(创建者优先,随后按注册顺序排列参与者)
自动关闭否
终止方式显式调用
channel.close()
或达到TTL(见下文“关闭通道”部分)
默认视图
NamedWindowedSummary(recent_n=N*2)
,其中
N
为参与者数量
默认预期设置
turn_within(120s, warn)
、
turn_within(600s, hide)
配置项
{"ordering": ORDERING_ROUND_ROBIN}
(目前仅支持此排序方式)
视图窗口大小设置为
N*2
,以便每个Agent的投影覆盖最近约两个完整轮次——既保证回复有足够上下文,又不会随着讨论推进导致提示词过度膨胀。

Discussion agents, the
NetworkPlugin
, and turn-taking

Discussion Agent、
NetworkPlugin
与轮次管理

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.
python
undefined
HubClient.register(...)
默认会附加
NetworkPlugin
——它添加了
peers
/
channels
/
tasks
/
context
/
delegate
这些身份级别的操作方法。
DiscussionAdapter.tools_for(...)
返回
[]
:discussion适配器不提供任何通道专属工具(无
say
方法)。参与者仅通过返回回复来完成自己的轮次——默认处理程序会将轮次结束时的
EV_TEXT(reply.body)
作为该轮的贡献内容发送。
因为没有
say
方法,discussion不存在“重复发送”的风险。轮次管理完全由
validate_send
强制执行:非
state.expected_next_speaker
发送的内容会被
ProtocolError
拒绝(提示“期望<其他参与者>发言”)。下文提到的
can_send
探测功能让每个处理程序在非自身轮次时跳过LLM调用,因此正常操作下不会出现违规发送的情况。
attach_plugin=False
仅移除
NetworkPlugin
的五个操作方法(
delegate
/
peers
/
channels
/
tasks
/
context
);对适配器工具无影响(discussion本身无工具)。可用于创建极简参与者。
python
undefined

Bare 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
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.
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
探测)

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 is
    expected_next_speaker
    ." Handler runs
    Agent.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):
python
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发送第一条消息时,中心节点会将
EV_TEXT
广播给Bob和Carol。两者的默认处理程序会并行触发:
  • Bob的处理程序 — 调用
    await hc.can_send(channel_id, bob.agent_id)
    。适配器返回“是,Bob是
    expected_next_speaker
    ”。处理程序执行
    Agent.ask
    ,发送Bob的回复。
  • Carol的处理程序 — 调用
    await hc.can_send(channel_id, carol.agent_id)
    。适配器返回“否,expected_next_speaker是Bob”。处理程序直接返回,不调用LLM。
当Bob的回复到达时,重复上述广播流程。此时
expected_next_speaker = carol
,因此Carol的处理程序会执行,Bob的则跳过。全程无不必要的LLM调用。
如果你为discussion通道编写自定义处理程序,请遵循此模式——并且将非文本消息委托给
default_handler
,这样可以保留
EV_CHANNEL_INVITE
的自动确认功能(否则通道会停留在
PENDING
状态,直到
invite_ack_timeout
触发,中心节点会自动关闭通道):
python
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

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.)
Discussion参与者不一定是
Agent
。注册一个
HumanClient
(
hc.register_human(Passport(name="user", kind="human"))
——详见
ag2-network-quickstart
→ “
HumanClient
”),并像其他参与者一样加入轮换顺序。它没有自动响应的通知处理程序,因此需要应用程序来驱动其轮次:使用
on_envelope(...)
订阅(或使用
next_envelope(...)
拉取),当消息到达时,检查
await hc.can_send(channel_id, user.agent_id)
(异步操作 — 必须使用await)——如果轮到人类用户,则提示用户输入并调用
await user.send(channel_id, text)
;否则等待。(框架自身的discussion测试正是如此实现的——人类用户根据适配器状态等待自己的轮次。)

State object

状态对象

python
@dataclass(slots=True)
class DiscussionState:
    participant_order: list[str]
    expected_next_speaker: str
    turn_count: int = 0
participant_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).
python
@dataclass(slots=True)
class DiscussionState:
    participant_order: list[str]
    expected_next_speaker: str
    turn_count: int = 0
participant_order
在创建时根据
Participant.order
排序后固定。每次接受
EV_TEXT
后,循环轮询会通过
(current_index + 1) % len(participant_order)
推进。可通过
hub._adapter_states[channel_id]
读取状态用于检查或测试(下划线表示“操作员API”——不建议用于生产环境Agent逻辑)。

Validation rules

验证规则

DiscussionAdapter.validate_send
rejects:
  • EV_TEXT
    from anyone other than
    state.expected_next_speaker
    — raises
    ProtocolError
    .
  • 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.
DiscussionAdapter.validate_send
会拒绝以下情况:
  • 非
    state.expected_next_speaker
    发送的
    EV_TEXT
    ——抛出
    ProtocolError
    。
  • 非参与者发送的内容。
  • 向已关闭的通道发送内容。
协议消息(
EV_CHANNEL_*
、
ag2.task.*
)不受轮次检查限制——它们属于管理类消息,不参与轮次交互。

Customising the ordering

自定义排序方式

Today only
ORDERING_ROUND_ROBIN
ships:
python
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
workflow
—
TransitionGraph.round_robin(participants, max_turns=N)
gives you the same shape with the option to add conditions later. See
ag2-network-workflow
.
目前仅支持
ORDERING_ROUND_ROBIN
:
python
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"
)
传入其他排序方式会在创建时抛出异常。未来的排序方式(动态、加权、优先级)将在此处扩展,且不会破坏现有循环轮询的兼容性。
如果你现在就需要条件式轮次管理,请切换到
workflow
适配器——
TransitionGraph.round_robin(participants, max_turns=N)
可提供相同的结构,且支持后续添加条件。详见
ag2-network-workflow
。

Closing

关闭通道

discussion
never auto-closes. Four patterns work; pick by who decides:
PatternWho decidesWhen
App-side cap →
channel.close()
Your orchestration codeTurn count, time, or any external predicate
Agent-side tool (via
ChannelInject
)
The LLMA participant decides "we're done"
Custom adapter subclassThe frameworkContent-based stop (rare for discussions)
TTL /
turn_within
expectations
The hub's sweepersSafety net only
discussion
永远不会自动关闭。有四种可行的关闭模式,可根据决策主体选择:
模式决策主体触发时机
应用端上限 →
channel.close()
你的编排代码达到轮次数量、时间限制或任何外部条件
Agent端工具(通过
ChannelInject
)
LLM某个参与者决定“讨论结束”
自定义适配器子类框架基于内容的停止(discussion场景中很少使用)
TTL /
turn_within
预期设置
中心节点的清理程序仅作为安全兜底

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
EV_CHANNEL_CLOSED.event_data["reason"]
— pick something descriptive so observers can distinguish a clean cap from a TTL or expectation violation.
python
await wait_for_text_count(hub, channel.channel_id, expected=6)
await channel.close(reason="cap_reached")
关闭原因会通过
EV_CHANNEL_CLOSED.event_data["reason"]
传递——请选择描述性的原因,以便观察者区分正常上限关闭与TTL或预期违规关闭。

Agent-side tool

Agent端工具

Same pattern as the other adapters — inject the active
Channel
, call
close()
:
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
ag2-network-quickstart
's "Five close routes" for the full picture.
与其他适配器模式相同——注入活动
Channel
,调用
close()
:
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-quickstart
中的“五种关闭方式”。

When
discussion
isn't enough

当
discussion
不足以满足需求时

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.
一旦你需要条件式轮次管理(“如果Bob提到截止日期,切换到deadlines_expert”),
discussion
就不再是合适的适配器。请迁移到
workflow
——它支持相同的N方结构,但轮次管理基于
TransitionGraph
,可配合
FromSpeaker
、
ToolCalled
和
ContextEquals
等条件。详见
ag2-network-workflow
中的迁移指南。

Default expectations

默认预期设置

ExpectationThresholdHandlerWhat 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
.
预期规则阈值处理方式作用
turn_within
120秒
warn
记录
AUDIT_KIND_EXPECTATION_VIOLATED
。通道保持开启。
turn_within
600秒
hide
在视图投影中将发言缓慢的参与者标记为“跳过”一个轮次。通道保持开启。
Discussion特意不通过预期设置自动关闭通道——发言缓慢的参与者不应该导致整个讨论终止。如果你希望通道在轮次停滞时终止,请通过
ag2-network-governance
将预期设置覆盖为
auto_close
。

Quick 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
type="discussion"
and
ordering="round_robin"
work too — the constants are equivalent and exist for type-safety in larger codebases.
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"
同样有效——常量的作用是在大型代码库中保证类型安全。