ag2-network-workflow

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AG2 Network — Workflow Adapter

AG2 网络 — 工作流适配器

workflow
is the orchestrated multi-party adapter. A declarative
TransitionGraph
describes who speaks first, what conditions fire on each turn, and when the channel terminates. It's the modern replacement for the classic
GroupChat + Agent.handoffs
pattern — turn-taking lives in the hub, not in an in-process speaker selector.
Prerequisite: read
ag2-network-quickstart
first. This skill assumes you know
Hub.open
,
HubClient.register
, the channel lifecycle, and the basic
agent_client.open(...)
/
channel.send(...)
flow.
workflow
是一款可编排的多方适配器。声明式
TransitionGraph
定义了谁先发言、每一轮触发的条件,以及通道何时终止。它是经典
GroupChat + Agent.handoffs
模式的现代替代方案——轮次控制权位于中心枢纽,而非进程内的发言选择器。
前置要求:先阅读
ag2-network-quickstart
。本技能默认你已掌握
Hub.open
、
HubClient.register
、通道生命周期,以及基础的
agent_client.open(...)
/
channel.send(...)
流程。

When to use

适用场景

Load this skill when the user asks for any of:
  • "Two/three/N agents with conditional handoffs"
  • "Pipeline / sequence of agents (researcher → writer → editor)"
  • "Triage agent routes to specialists"
  • "Drafter / reviewer feedback loop"
  • "Coordinator + specialists / hierarchical orchestration"
  • "Migrate from
    GroupChat
    /
    ConversableAgent.handoffs
    /
    ReplyResult(target=...)
    "
  • "I want context-driven routing (
    OnContextCondition
    ,
    StringContextCondition
    )"
Don't use
workflow
for:
  • Strict 1Q1R between two agents →
    consulting
    (see
    ag2-network-quickstart
    ).
  • Free-form 2-party chat with no turn order →
    conversation
    (see
    ag2-network-quickstart
    ).
  • Fixed round-robin with no conditions →
    discussion
    (see
    ag2-network-discussion
    ).
If the user is unsure, the rule of thumb: need any condition more complex than "next speaker in a fixed list"? Use
workflow
.
当用户提出以下需求时,加载本技能:
  • "两个/三个/N个Agent之间的条件切换"
  • "Agent流水线/序列(研究员→撰稿人→编辑)"
  • "分诊Agent将请求路由给专家"
  • "起草人/审核人反馈循环"
  • "协调者+专家/分层编排"
  • "从
    GroupChat
    /
    ConversableAgent.handoffs
    /
    ReplyResult(target=...)
    迁移"
  • "我需要基于上下文的路由(
    OnContextCondition
    、
    StringContextCondition
    )"
不适用
workflow
的场景:
  • 两个Agent之间严格的一问一答 → 使用
    consulting
    (参见
    ag2-network-quickstart
    )。
  • 无轮次顺序的自由两方对话 → 使用
    conversation
    (参见
    ag2-network-quickstart
    )。
  • 无条件的固定轮询 → 使用
    discussion
    (参见
    ag2-network-discussion
    )。
若用户不确定,可遵循经验法则:需要比「固定列表中的下一位发言者」更复杂的条件?就用
workflow
。

Shape

特性参数

Participants2+
Turn orderWhatever
TransitionGraph
says
Auto-closeYes — when graph emits
TerminateTarget
or
max_turns
is hit
Default view
NamedWindowedSummary(recent_n=N*2)
Default expectations
turn_within(120s, warn)
,
turn_within(600s, auto_close)
Required knob
{"graph": <TransitionGraph.to_dict()>}
The expectation is strict: a stuck speaker auto-closes the channel after 10 minutes. Tune via
ag2-network-governance
.
参与者2+
轮次顺序由
TransitionGraph
定义
自动关闭是——当图输出
TerminateTarget
或达到
max_turns
时触发
默认视图
NamedWindowedSummary(recent_n=N*2)
默认规则
turn_within(120s, warn)
、
turn_within(600s, auto_close)
必填配置
{"graph": <TransitionGraph.to_dict()>}
规则严格:发言者停滞10分钟后,通道将自动关闭。可通过
ag2-network-governance
调整。

Workflow agents and the
NetworkPlugin

工作流Agent与
NetworkPlugin

HubClient.register(...)
attaches
NetworkPlugin
by default. The plugin adds only the identity-level cross-cutting tools —
peers
/
channels
/
tasks
/
context
/
delegate
. Channel-specific tools (notably
say
, plus your workflow handoff
@tool
s) come from
adapter.tools_for(client, metadata, state, participant_id)
, resolved per turn by the default handler and merged into
agent.ask(tools=...)
— and the
workflow
adapter offers none
:
WorkflowAdapter.tools_for(...)
returns
[]
. So a workflow agent never sees
say
; routing is entirely your
@tool
-decorated handoff functions (returning
Handoff
, mutating context vars, or calling
channel.close()
).
Consequence: the old "mid-turn
say
races the round-end
EV_PACKET
" hazard can't happen on a workflow channel — you do not need
attach_plugin=False
for correctness. Choose by capability instead:
  • Keep the plugin (default) if the agent should also
    delegate
    to a peer, look up
    peers
    /
    channels
    /
    tasks
    , or read/write channel
    context
    from outside the graph. (
    delegate
    /
    channels(action="open")
    spin up separate channels — they never emit on the workflow channel, so they don't disturb its turn machinery.)
  • attach_plugin=False
    for a genuinely bare agent — only the domain
    @tool
    s you wrote. Tidy for pure pipeline workers and most tests.
python
undefined
HubClient.register(...)
默认会附加
NetworkPlugin
。该插件仅添加身份级跨域工具——
peers
/
channels
/
tasks
/
context
/
delegate
。通道专属工具(尤其是
say
,以及你的工作流切换
@tool
s)来自
adapter.tools_for(client, metadata, state, participant_id)
,由默认处理程序每轮解析并合并到
agent.ask(tools=...)
中——而**
workflow
适配器不提供任何工具**:
WorkflowAdapter.tools_for(...)
返回
[]
。因此工作流Agent永远不会看到
say
;路由完全由你用
@tool
装饰的切换函数控制(返回
Handoff
、修改上下文变量,或调用
channel.close()
)。
由此带来的好处:旧模式中「轮次中途
say
与轮次结束
EV_PACKET
竞争」的风险在workflow通道中完全不会发生——你无需设置
attach_plugin=False
来保证正确性。可根据功能需求选择:
  • 保留插件(默认):若Agent需要向对等方
    delegate
    、查询
    peers
    /
    channels
    /
    tasks
    ,或从外部读取/写入通道
    context
    。(
    delegate
    /
    channels(action="open")
    会启动独立通道——它们不会在workflow通道上输出内容,因此不会干扰轮次机制。)
  • 设置
    attach_plugin=False
    :用于纯流水线工作者和大部分测试场景,仅保留你编写的领域
    @tool
    s。
python
undefined

Bare pipeline worker — no cross-cutting network tools:

纯流水线工作者——无跨域网络工具:

worker = await wc.register(agent, Passport(name="worker"), Resume(), attach_plugin=False)

The default handler wraps the LLM's plain-text reply into the workflow `EV_PACKET` either way. The examples below pass `attach_plugin=False` because they're pure pipeline workers; drop it if your agents need `delegate` / `peers` / etc. Full detail: `ag2-network-tools-and-views` → "Adapter-owned tools (`tools_for`)".
worker = await wc.register(agent, Passport(name="worker"), Resume(), attach_plugin=False)

无论哪种情况,默认处理程序都会将LLM的纯文本回复封装到工作流`EV_PACKET`中。以下示例使用`attach_plugin=False`,因为它们是纯流水线工作者;若你的Agent需要`delegate` / `peers`等功能,可移除该参数。详情见:`ag2-network-tools-and-views` → "适配器专属工具(`tools_for`)"。

Smallest example — pipeline via
sequence

最小示例——通过
sequence
实现流水线

python
import asyncio

from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.knowledge import MemoryKnowledgeStore
from ag2.network import (
    EV_CHANNEL_CLOSED,
    Hub,
    HubClient,
    LocalLink,
    Passport,
    Resume,
    TransitionGraph,
)


async def main() -> None:
    config = AnthropicConfig(model="claude-sonnet-4-6")
    hub = await Hub.open(MemoryKnowledgeStore(), ttl_sweep_interval=0)
    link = LocalLink(hub)

    rhc, whc, ehc = (HubClient(link, hub=hub) for _ in range(3))

    researcher = await rhc.register(
        Agent("researcher", prompt="Bullet-point three facts on the topic.", config=config),
        Passport(name="researcher"), Resume(), attach_plugin=False,
    )
    writer = await whc.register(
        Agent("writer", prompt="Turn the bullets into one paragraph.", config=config),
        Passport(name="writer"), Resume(), attach_plugin=False,
    )
    editor = await ehc.register(
        Agent("editor", prompt="Tighten the paragraph. Reply with the final text.", config=config),
        Passport(name="editor"), Resume(), attach_plugin=False,
    )

    graph = TransitionGraph.sequence([
        researcher.agent_id, writer.agent_id, editor.agent_id,
    ])

    channel = await researcher.open(
        type="workflow",
        target=[writer.agent_id, editor.agent_id],
        knobs={"graph": graph.to_dict()},
    )
    await channel.send("Topic: how does HTTPS work?")

    close_env = await researcher.wait_for_channel_event(
        channel_id=channel.channel_id,
        predicate=lambda e: e.event_type == EV_CHANNEL_CLOSED,
        timeout=180.0,
    )
    print(f"closed: {close_env.event_data.get('reason')!r}")  # 'sequence_complete'

    for hc in (rhc, whc, ehc):
        await hc.close()
    await hub.close()


asyncio.run(main())
TransitionGraph.sequence([a, b, c])
builds three
FromSpeaker(x) → AgentTarget(next)
transitions plus a
TerminateTarget("sequence_complete")
default. After
c
speaks no transition matches, the default fires, and the channel auto-closes.
python
import asyncio

from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.knowledge import MemoryKnowledgeStore
from ag2.network import (
    EV_CHANNEL_CLOSED,
    Hub,
    HubClient,
    LocalLink,
    Passport,
    Resume,
    TransitionGraph,
)


async def main() -> None:
    config = AnthropicConfig(model="claude-sonnet-4-6")
    hub = await Hub.open(MemoryKnowledgeStore(), ttl_sweep_interval=0)
    link = LocalLink(hub)

    rhc, whc, ehc = (HubClient(link, hub=hub) for _ in range(3))

    researcher = await rhc.register(
        Agent("researcher", prompt="Bullet-point three facts on the topic.", config=config),
        Passport(name="researcher"), Resume(), attach_plugin=False,
    )
    writer = await whc.register(
        Agent("writer", prompt="Turn the bullets into one paragraph.", config=config),
        Passport(name="writer"), Resume(), attach_plugin=False,
    )
    editor = await ehc.register(
        Agent("editor", prompt="Tighten the paragraph. Reply with the final text.", config=config),
        Passport(name="editor"), Resume(), attach_plugin=False,
    )

    graph = TransitionGraph.sequence([
        researcher.agent_id, writer.agent_id, editor.agent_id,
    ])

    channel = await researcher.open(
        type="workflow",
        target=[writer.agent_id, editor.agent_id],
        knobs={"graph": graph.to_dict()},
    )
    await channel.send("Topic: how does HTTPS work?")

    close_env = await researcher.wait_for_channel_event(
        channel_id=channel.channel_id,
        predicate=lambda e: e.event_type == EV_CHANNEL_CLOSED,
        timeout=180.0,
    )
    print(f"closed: {close_env.event_data.get('reason')!r}")  # 'sequence_complete'

    for hc in (rhc, whc, ehc):
        await hc.close()
    await hub.close()


asyncio.run(main())
TransitionGraph.sequence([a, b, c])
会构建三个
FromSpeaker(x) → AgentTarget(next)
转换,以及一个默认的
TerminateTarget("sequence_complete")
。
c
发言后,若没有匹配的转换,默认规则触发,通道自动关闭。

TransitionGraph
anatomy

TransitionGraph
结构

python
@dataclass(slots=True)
class TransitionGraph:
    initial_speaker: str            # agent_id of the first speaker (must be a participant)
    transitions: list[Transition]   # ordered list, walked in priority order on every fold
    default_target: TransitionTarget  # consulted if no transition matches
    max_turns: int | None = None    # hard turn cap; force-terminates on reaching


@dataclass(slots=True)
class Transition:
    when: TransitionCondition       # evaluated against the just-accepted envelope + state
    then: TransitionTarget          # if when() is True, this resolves the next speaker
    priority: int = 0               # LOWER runs first; ties break by insertion order
On every accepted substantive envelope (text or packet), the adapter walks the
transitions
list. The first matching condition's target resolves the next speaker. If none match,
default_target
is consulted. A
TerminateTarget
resolves with
next_speaker=None
, which makes the adapter return
AdapterResult(next_state=CLOSING, ...)
and the hub posts
EV_CHANNEL_CLOSED
with that target's
reason
.
max_turns
is a separate, harder cap: once
turn_count
reaches it the adapter closes the channel immediately with reason
"max_turns"
— it does not fall through to
default_target
(so a
default_target=TerminateTarget("…")
reason only appears when an actual turn produced no matching transition).
priority
sorts ascending — a lower number is checked first.
The adapter does
sorted(transitions, key=lambda t: t.priority)
then walks the result, so
priority=0
(the default) beats
priority=100
. If you want a rule consulted before the others, either put it at the top of the list (the sort is stable, so equal priorities keep list order) or give it a negative priority. A common mistake: writing
priority=100
on a
ContextEquals(...) → TerminateTarget(...)
rule expecting it to win — it gets pushed to the end and a
FromSpeaker(...)
fallback fires first instead, so the channel never terminates.
python
@dataclass(slots=True)
class TransitionGraph:
    initial_speaker: str            # 第一个发言者的agent_id(必须是参与者)
    transitions: list[Transition]   # 有序列表,每轮按优先级遍历
    default_target: TransitionTarget  # 无匹配转换时使用的目标
    max_turns: int | None = None    # 硬轮次上限;达到后强制终止


@dataclass(slots=True)
class Transition:
    when: TransitionCondition       # 根据刚接收的信封和状态评估
    then: TransitionTarget          # 若when()为True,解析下一位发言者
    priority: int = 0               # 数值越小优先级越高;优先级相同时按插入顺序处理
每接收一个有效信封(文本或数据包),适配器都会遍历
transitions
列表。第一个匹配条件的目标将解析下一位发言者。若无匹配,则使用
default_target
。
TerminateTarget
会返回
next_speaker=None
,使适配器返回
AdapterResult(next_state=CLOSING, ...)
,同时枢纽会发布
EV_CHANNEL_CLOSED
事件,并携带该目标的
reason
。
max_turns
是独立的硬上限:一旦
turn_count
达到该值,适配器会立即关闭通道,原因是**
"max_turns"
**——不会回退到
default_target
(因此
default_target=TerminateTarget("…")
的原因仅在某一轮无匹配转换时才会出现)。
priority
按升序排序——数值越小越先被检查。
适配器会执行
sorted(transitions, key=lambda t: t.priority)
,然后遍历结果,因此默认的
priority=0
优先级高于
priority=100
。若希望某条规则优先被检查,可将其放在列表顶部(排序是稳定的,优先级相同时保持列表顺序),或设置负数优先级。常见错误:为
ContextEquals(...) → TerminateTarget(...)
规则设置
priority=100
,期望它优先触发——但实际上它会被排到最后,
FromSpeaker(...)
回退规则会先触发,导致通道永远不会终止。

Gotcha — the kickoff
channel.send()
is the
initial_speaker
's first turn

注意事项——启动时的
channel.send()
是
initial_speaker
的第一轮发言

When you do
agent.open(type="workflow", ...)
then
await channel.send(text)
, that first send is folded as
initial_speaker
's turn — it's an envelope from that agent, and
expected_next_speaker
starts equal to
initial_speaker
, so
validate_send
accepts it and the graph immediately routes onward. The
initial_speaker
's
Agent.ask
never runs for that turn
— the text you sent is its contribution.
So if your design wants the first agent that should actually think (the drafter, the researcher, the planner) to respond to the kickoff prompt, that agent must not be
initial_speaker
and must not be the one calling
channel.send()
. The clean answer is a
HumanClient
as the seeder — a non-LLM participant (no
Agent
, no plugin) whose only job is to open the channel and post the brief. Make it
initial_speaker
and route
FromSpeaker(user) → AgentTarget(drafter)
:
python
from ag2.network import HumanClient, Passport  # plus the usual imports

user_hc = HubClient(link, hub=hub)
user = await user_hc.register_human(Passport(name="user", kind="human"))   # ← register_human, not register
drafter  = await drf_hc.register(drafter_agent,  Passport(name="drafter"),  Resume(), attach_plugin=False)
reviewer = await rev_hc.register(reviewer_agent, Passport(name="reviewer"), Resume(), attach_plugin=False)

graph = TransitionGraph(
    initial_speaker=user.agent_id,
    transitions=[
        Transition(when=ContextEquals("approved", value=True), then=TerminateTarget("approved")),
        Transition(when=FromSpeaker(user.agent_id),     then=AgentTarget(drafter.agent_id)),    # brief → drafter
        Transition(when=FromSpeaker(drafter.agent_id),  then=AgentTarget(reviewer.agent_id)),
        Transition(when=FromSpeaker(reviewer.agent_id), then=AgentTarget(drafter.agent_id)),    # revise loop
    ],
    default_target=TerminateTarget("max_revisions"),
    max_turns=12,
)

channel = await user.open(type="workflow", target=[drafter.agent_id, reviewer.agent_id], knobs={"graph": graph.to_dict()})
await channel.send("Brief: …")   # consumed as `user`'s turn → routes to `drafter`, who drafts
当你执行
agent.open(type="workflow", ...)
然后
await channel.send(text)
时,第一次发送会被视为
initial_speaker
的轮次——这是该Agent发送的信封,且
expected_next_speaker
初始值等于
initial_speaker
,因此
validate_send
会接受它,图会立即路由到下一位发言者。
initial_speaker
的
Agent.ask
在这一轮不会运行
——你发送的文本就是它的贡献内容。
因此,若你的设计希望实际执行思考的第一个Agent(起草人、研究员、规划师)响应启动提示,该Agent不能是
initial_speaker
,也不能是调用
channel.send()
的对象。最佳方案是使用**
HumanClient
**作为启动者——一个非LLM参与者(无
Agent
、无插件),仅负责打开通道并发布提示。将其设为
initial_speaker
,并设置路由规则
FromSpeaker(user) → AgentTarget(drafter)
:
python
from ag2.network import HumanClient, Passport  # 加上常用导入

user_hc = HubClient(link, hub=hub)
user = await user_hc.register_human(Passport(name="user", kind="human"))   # ← 使用register_human,而非register
drafter  = await drf_hc.register(drafter_agent,  Passport(name="drafter"),  Resume(), attach_plugin=False)
reviewer = await rev_hc.register(reviewer_agent, Passport(name="reviewer"), Resume(), attach_plugin=False)

graph = TransitionGraph(
    initial_speaker=user.agent_id,
    transitions=[
        Transition(when=ContextEquals("approved", value=True), then=TerminateTarget("approved")),
        Transition(when=FromSpeaker(user.agent_id),     then=AgentTarget(drafter.agent_id)),    # 提示→起草人
        Transition(when=FromSpeaker(drafter.agent_id),  then=AgentTarget(reviewer.agent_id)),
        Transition(when=FromSpeaker(reviewer.agent_id), then=AgentTarget(drafter.agent_id)),    # 修改循环
    ],
    default_target=TerminateTarget("max_revisions"),
    max_turns=12,
)

channel = await user.open(type="workflow", target=[drafter.agent_id, reviewer.agent_id], knobs={"graph": graph.to_dict()})
await channel.send("Brief: …")   # 作为`user`的轮次被消费→路由到`drafter`,由其起草

Later, when the channel closes: await user.next_envelope(predicate=lambda e: e.event_type == EV_CHANNEL_CLOSED, timeout=300)

之后,通道关闭时:await user.next_envelope(predicate=lambda e: e.event_type == EV_CHANNEL_CLOSED, timeout=300)


`register_human(passport, *, resume=None, rule=None, auto_ack_invites=True)` returns a `HumanClient` (`HubClient.register(...)` now *rejects* `kind="human"` and points you here). It auto-acks invites so the quorum handshake completes without UI round-trips; pass `auto_ack_invites=False` to gate joins by hand. Drive it with `on_envelope(callback)` (push) or `next_envelope(...)` / `envelopes()` (pull) — see `ag2-network-quickstart` → "`HumanClient`".

If you'd rather not introduce a `HumanClient`, the older workaround still works: a throwaway "intake" `Agent` as `initial_speaker` whose `Agent.ask` is never invoked (no transition routes back to it) — but a `HumanClient` is cheaper (no model config, no plugin) and is what the framework's own smoke tests use. Either way, the same applies to `TransitionGraph.sequence([a, b, c])`: `channel.send(...)` is `a`'s turn, so if `a` is supposed to *produce* the first artifact (not just relay a prompt), prepend a seeder — `sequence([user, a, b, c])`.

(Symptom when you get this wrong: the second agent in the chain receives the bare kickoff prompt as if it were the first agent's output, "responds" to something that isn't there, and the pipeline produces nonsense — often hallucinating that an artifact exists.)

`register_human(passport, *, resume=None, rule=None, auto_ack_invites=True)`返回`HumanClient`(`HubClient.register(...)`现在会拒绝`kind="human"`,并引导你使用此方法)。它会自动确认邀请,无需UI往返即可完成法定人数握手;若需手动控制加入,可设置`auto_ack_invites=False`。可通过`on_envelope(callback)`(推送)或`next_envelope(...)` / `envelopes()`(拉取)来驱动它——详见`ag2-network-quickstart` → "`HumanClient`"。

若你不想引入`HumanClient`,旧的解决方法仍然有效:使用一次性的"接收"`Agent`作为`initial_speaker`,其`Agent.ask`永远不会被调用(没有转换路由回它)——但`HumanClient`更轻量化(无需模型配置、无插件),也是框架自身冒烟测试使用的方案。无论哪种方式,`TransitionGraph.sequence([a, b, c])`的规则都相同:`channel.send(...)`是`a`的轮次,因此若`a`需要生成第一个产物(而非仅转发提示),需在序列前添加启动者——`sequence([user, a, b, c])`。

(错误配置的症状:链中的第二个Agent会收到原始启动提示,仿佛它是第一个Agent的输出,"响应"不存在的内容,导致流水线产生无意义的结果——通常会幻觉某个产物已存在。)

Built-in targets

内置目标

TargetDecision
AgentTarget(agent_id)
Hand off to a specific named agent.
RoundRobinTarget()
Advance through the participant order.
StayTarget()
Same speaker continues (rare; "let me elaborate" patterns).
RevertToInitiatorTarget()
Hand back to whoever opened the channel.
TerminateTarget(reason="…")
End the channel; reason flows on
EV_CHANNEL_CLOSED.event_data["reason"]
.
目标决策
AgentTarget(agent_id)
切换到指定的Agent。
RoundRobinTarget()
按参与者顺序推进。
StayTarget()
同一发言者继续(罕见;用于"让我详细说明"等场景)。
RevertToInitiatorTarget()
切换回打开通道的参与者。
TerminateTarget(reason="…")
结束通道;原因会携带在
EV_CHANNEL_CLOSED.event_data["reason"]
中。

Built-in conditions

内置条件

ConditionFires when
Always()
Every accepted envelope.
FromSpeaker(agent_id)
The just-accepted envelope was sent by this agent.
ToolCalled(tool_name)
The previous turn called this tool by name (matched via
routing.tool
on the packet).
ContextEquals(key, value)
Channel-scoped
context_vars[key]
equals
value
. Missing keys compare as
None
.
For combinations beyond what these compose to, see "Custom targets / conditions" below.
条件触发时机
Always()
每接收一个有效信封时。
FromSpeaker(agent_id)
刚接收的信封由该Agent发送。
ToolCalled(tool_name)
上一轮调用了指定名称的工具(通过数据包中的
routing.tool
匹配)。
ContextEquals(key, value)
通道级
context_vars[key]
等于
value
。不存在的键视为
None
。
若需要这些内置条件无法组合实现的逻辑,见下文"自定义目标/条件"。

Convenience factories

便捷工厂方法

python
undefined
python
undefined

Pipeline: a → b → c → terminate.

流水线:a → b → c → 终止。

graph = TransitionGraph.sequence([alice.agent_id, bob.agent_id, carol.agent_id])
graph = TransitionGraph.sequence([alice.agent_id, bob.agent_id, carol.agent_id])

Round-robin with a cap: a → b → c → a → b → c → terminate after 6 turns.

带上限的轮询:a → b → c → a → b → c → 6轮后终止。

graph = TransitionGraph.round_robin( participants=[alice.agent_id, bob.agent_id, carol.agent_id], max_turns=6, )

`round_robin` uses `Always() → RoundRobinTarget()` and a `TerminateTarget("round_robin_complete")` default — but since `Always()` matches every turn, the default is unreachable and the channel actually closes with reason `"max_turns"` once the cap is hit (set `max_turns` or it cycles forever). `sequence` uses `FromSpeaker(steps[i]) → AgentTarget(steps[i+1])` chains with a `TerminateTarget("sequence_complete")` default — *that* one is reachable: after the last step speaks, no `FromSpeaker` rule matches, so the default fires and you get `"sequence_complete"`.
graph = TransitionGraph.round_robin( participants=[alice.agent_id, bob.agent_id, carol.agent_id], max_turns=6, )

`round_robin`使用`Always() → RoundRobinTarget()`和默认的`TerminateTarget("round_robin_complete")`——但由于`Always()`匹配每一轮,默认规则永远不会触发,通道实际上会在达到上限时以`"max_turns"`为由关闭(必须设置`max_turns`,否则会无限循环)。`sequence`使用`FromSpeaker(steps[i]) → AgentTarget(steps[i+1])`链,以及默认的`TerminateTarget("sequence_complete")`——这个默认规则是可达的:最后一步发言后,没有`FromSpeaker`规则匹配,因此默认规则触发,你会得到`"sequence_complete"`的关闭原因。

Conditional handoff (the most common manual graph)

条件切换(最常见的手动图)

A triage agent inspects each request and routes to a specialist via a tool call:
python
from ag2.network import (
    AgentTarget, FromSpeaker, RevertToInitiatorTarget,
    TerminateTarget, ToolCalled, Transition, TransitionGraph,
)

graph = TransitionGraph(
    initial_speaker=triage.agent_id,
    transitions=[
        # Triage's `escalate` tool fired → route to security.
        Transition(when=ToolCalled("escalate"),
                   then=AgentTarget(security.agent_id)),
        # Once security has spoken, hand back to whoever opened the channel.
        Transition(when=FromSpeaker(security.agent_id),
                   then=RevertToInitiatorTarget()),
        # Otherwise triage's reply goes to the generic responder.
        Transition(when=FromSpeaker(triage.agent_id),
                   then=AgentTarget(general.agent_id)),
    ],
    default_target=TerminateTarget(reason="triage_complete"),
    max_turns=20,
)
For each
ToolCalled(name) → AgentTarget(agent)
transition, attach an
@tool
-decorated function on the speaker's
Agent
. The simplest form returns a typed
Handoff
— the framework reads it from the tool's result and routes:
python
from ag2.network import Handoff
分诊Agent检查每个请求,并通过工具调用路由给专家:
python
from ag2.network import (
    AgentTarget, FromSpeaker, RevertToInitiatorTarget,
    TerminateTarget, ToolCalled, Transition, TransitionGraph,
)

graph = TransitionGraph(
    initial_speaker=triage.agent_id,
    transitions=[
        # 分诊的`escalate`工具触发→路由到安全专家。
        Transition(when=ToolCalled("escalate"),
                   then=AgentTarget(security.agent_id)),
        # 安全专家发言后,切换回打开通道的参与者。
        Transition(when=FromSpeaker(security.agent_id),
                   then=RevertToInitiatorTarget()),
        # 否则分诊的回复发送给通用响应者。
        Transition(when=FromSpeaker(triage.agent_id),
                   then=AgentTarget(general.agent_id)),
    ],
    default_target=TerminateTarget(reason="triage_complete"),
    max_turns=20,
)
对于每个
ToolCalled(name) → AgentTarget(agent)
转换,需在发言者的
Agent
上附加一个
@tool
装饰的函数。最简单的形式是返回类型化的
Handoff
——框架会从工具结果中读取它并进行路由:
python
from ag2.network import Handoff

.tool
lives on
Agent
. The
AgentClient
returned by
hc.register(...)

.tool
属于
Agent
。
hc.register(...)
返回的
AgentClient
是网络句柄(用于
.open()
、
.send()
、
.agent_id
),

is the network handle (used for
.open()
,
.send()
,
.agent_id
) and does

不暴露
.tool
。因此:先构建Agent,附加工具,再注册。

NOT expose
.tool
. So: build the Agent, attach tools, then register.

—
triage_agent = Agent("triage", prompt="…", config=config)
@triage_agent.tool async def escalate(reason: str = "") -> Handoff: """Escalate this ticket to the security reviewer.""" # Handoff.target is the participant's Passport NAME, not a raw agent_id — # the framework resolves name → id at packet-finalize via the hub's name directory. return Handoff(target="security", reason=reason)
triage = await triage_hc.register( triage_agent, Passport(name="triage"), Resume(), attach_plugin=False, )

The typed `Handoff` return supersedes the matching `ToolCalled` rule when both are configured — useful when the *target* depends on runtime state. The graph's `ToolCalled` rule remains useful as documentation and as a fallback.
triage_agent = Agent("triage", prompt="…", config=config)
@triage_agent.tool async def escalate(reason: str = "") -> Handoff: """将此工单升级给安全审核员。""" # Handoff.target是参与者的Passport名称,而非原始agent_id—— # 框架会在数据包最终确定时,通过枢纽的名称目录将名称解析为id。 return Handoff(target="security", reason=reason)
triage = await triage_hc.register( triage_agent, Passport(name="triage"), Resume(), attach_plugin=False, )

当同时配置了类型化`Handoff`返回和匹配的`ToolCalled`规则时,`Handoff`会覆盖规则——当目标依赖运行时状态时非常有用。图中的`ToolCalled`规则仍可作为文档和回退方案。

Context variables — the read/write loop

上下文变量——读写循环

Channel-scoped mutable state lives on
WorkflowState.context_vars: dict[str, Any]
, scoped to one workflow channel and persisted as
EV_CONTEXT_SET
envelopes on the WAL.
通道级可变状态存储在
WorkflowState.context_vars: dict[str, Any]
中,作用域为单个工作流通道,并作为
EV_CONTEXT_SET
信封持久化到WAL中。

Writing context (from a tool)

写入上下文(从工具中)

python
from ag2.network import ChannelInject, EV_CONTEXT_SET
from ag2.network.workflow_helpers import set_context


async def set_route(route: str, channel: ChannelInject) -> str:
    """Record the routing decision for this channel."""
    if channel is None:
        return "no active channel"
    await set_context(channel, "route", route)
    return f"route set to {route!r}"
set_context(channel, key, value)
is a thin wrapper that emits
EV_CONTEXT_SET
with the default broadcast audience (
audience=None
— visible to every participant; the mutation lands on
WorkflowState.context_vars
after fold). Equivalent raw form:
python
await channel.send(
    "",
    event_type=EV_CONTEXT_SET,
    event_data={"set": {"route": route}},
)
The full event_data shape supports both set and delete:
{"set": {...}, "delete": [...]}
. Either is optional; within one envelope
delete
runs before
set
.
python
from ag2.network import ChannelInject, EV_CONTEXT_SET
from ag2.network.workflow_helpers import set_context


async def set_route(route: str, channel: ChannelInject) -> str:
    """记录此通道的路由决策。"""
    if channel is None:
        return "无活动通道"
    await set_context(channel, "route", route)
    return f"路由已设置为{route!r}"
set_context(channel, key, value)
是一个轻量封装,会发送默认广播受众的
EV_CONTEXT_SET
事件(
audience=None
——所有参与者可见;变更会在折叠后应用到
WorkflowState.context_vars
)。等效的原始形式:
python
await channel.send(
    "",
    event_type=EV_CONTEXT_SET,
    event_data={"set": {"route": route}},
)
完整的event_data格式支持设置和删除:
{"set": {...}, "delete": [...]}
。两者都是可选的;在同一个信封中,
delete
会先于
set
执行。

Reading context (in a transition)

读取上下文(在转换中)

python
Transition(
    when=ContextEquals(key="route", value="security"),
    then=AgentTarget(security.agent_id),
)
ContextEquals(key, value=None)
fires when the key is unset or explicitly deleted. The mutation is folded before substantive turn checks, so a
ContextEquals
rule on the same fold sees the just-set value.
python
Transition(
    when=ContextEquals(key="route", value="security"),
    then=AgentTarget(security.agent_id),
)
当键未设置或被显式删除时,
ContextEquals(key, value=None)
会触发。变更会在实质性轮次检查之前被折叠,因此同一轮次的
ContextEquals
规则会看到刚设置的值。

Reading context (in a tool)

读取上下文(在工具中)

python
from ag2.network import ChannelStateInject


async def increment_counter(channel: ChannelInject, state: ChannelStateInject) -> str:
    if state is None or channel is None:
        return "no active channel"
    current = state.context_vars.get("counter", 0)
    await set_context(channel, "counter", current + 1)
    return f"counter now {current + 1}"
python
from ag2.network import ChannelStateInject


async def increment_counter(channel: ChannelInject, state: ChannelStateInject) -> str:
    if state is None or channel is None:
        return "无活动通道"
    current = state.context_vars.get("counter", 0)
    await set_context(channel, "counter", current + 1)
    return f"计数器现在为{current + 1}"

Initial values

初始值

python
channel = await alice.open(
    type="workflow",
    target=[bob.agent_id, carol.agent_id],
    knobs={
        "graph": graph.to_dict(),
        "context_vars": {"escalation_level": 0, "ticket_id": ticket_id},
    },
)
python
channel = await alice.open(
    type="workflow",
    target=[bob.agent_id, carol.agent_id],
    knobs={
        "graph": graph.to_dict(),
        "context_vars": {"escalation_level": 0, "ticket_id": ticket_id},
    },
)

The stuck-routing trap

路由循环陷阱

ContextEquals
is sticky — once a key is set, every subsequent fold re-evaluates it. If you have
ContextEquals("route", "security") → AgentTarget(security)
near the top of the list, you'll bounce back to security forever (or until
max_turns
). Two fixes:
  1. List terminate rules before context-conditions:
    FromSpeaker(security) → TerminateTarget(...)
    placed earlier short-circuits the loop after security speaks.
  2. Have the routed agent clear the key: the security agent's tool emits
    EV_CONTEXT_SET
    with
    {"delete": ["route"]}
    when done.
Fix #1 is the more common pattern.
ContextEquals
是粘性的——一旦设置了键,后续每一轮都会重新评估。若你在列表顶部设置了
ContextEquals("route", "security") → AgentTarget(security)
,则会永远跳回安全专家(直到达到
max_turns
)。两种解决方法:
  1. 将终止规则放在上下文条件之前:
    FromSpeaker(security) → TerminateTarget(...)
    放在前面,可在安全专家发言后终止循环。
  2. 让被路由的Agent清除键:安全专家的工具发送
    EV_CONTEXT_SET
    事件,其中包含
    {"delete": ["route"]}
    。
方法1是更常见的模式。

Feedback loop — context-driven termination

反馈循环——基于上下文的终止

python
graph = TransitionGraph(
    initial_speaker=intake.agent_id,
    transitions=[
        # Approve sets done=True → terminate.
        Transition(when=ContextEquals("done", value=True),
                   then=TerminateTarget("approved")),
        # intake → drafter.
        Transition(when=FromSpeaker(intake.agent_id),
                   then=AgentTarget(drafter.agent_id)),
        # Alternate drafter ↔ reviewer.
        Transition(when=FromSpeaker(drafter.agent_id),
                   then=AgentTarget(reviewer.agent_id)),
        Transition(when=FromSpeaker(reviewer.agent_id),
                   then=AgentTarget(drafter.agent_id)),
    ],
    default_target=TerminateTarget("max_iterations"),
    max_turns=10,
)
python
graph = TransitionGraph(
    initial_speaker=intake.agent_id,
    transitions=[
        # 批准设置done=True→终止。
        Transition(when=ContextEquals("done", value=True),
                   then=TerminateTarget("approved")),
        # intake→drafter。
        Transition(when=FromSpeaker(intake.agent_id),
                   then=AgentTarget(drafter.agent_id)),
        # drafter与reviewer交替。
        Transition(when=FromSpeaker(drafter.agent_id),
                   then=AgentTarget(reviewer.agent_id)),
        Transition(when=FromSpeaker(reviewer.agent_id),
                   then=AgentTarget(drafter.agent_id)),
    ],
    default_target=TerminateTarget("max_iterations"),
    max_turns=10,
)

reviewer_agent
is the
Agent
;
reviewer
is the
AgentClient
returned

reviewer_agent
是
Agent
;
reviewer
是
register()
返回的
AgentClient
,用于上述图中的
reviewer.agent_id
。

by
register()
and used in the graph above (for
reviewer.agent_id
).

工具必须在注册前附加到Agent。

Tools must be attached to the Agent before registration.

—
@reviewer_agent.tool async def approve(reason: str, channel: ChannelInject) -> str: """Mark the draft approved; the graph terminates with reason='approved'.""" if channel is None: return "no channel" await set_context(channel, "done", True) return f"approved: {reason}"

The reviewer's `approve` call writes `done=True`; the reviewer's reply text lands on the same packet fold; `ContextEquals("done", True)` matches and the channel terminates with `"approved"`. Without `approve`, the drafter/reviewer alternation continues until `turn_count` hits `max_turns` and the channel closes with reason `"max_turns"`. (The `default_target=TerminateTarget("max_iterations")` above only fires if some turn produced *no* matching transition — here one of the `FromSpeaker` rules always matches, so in this graph it's effectively dead; keep it as a sane fallback for graphs where it can be reached.)
@reviewer_agent.tool async def approve(reason: str, channel: ChannelInject) -> str: """标记草稿已批准;图会以reason='approved'终止。""" if channel is None: return "无通道" await set_context(channel, "done", True) return f"已批准:{reason}"

审核员调用`approve`会写入`done=True`;审核员的回复文本会在同一数据包轮次中处理;`ContextEquals("done", True)`匹配,通道以`"approved"`为由终止。若未调用`approve`,drafter/reviewer会交替直到`turn_count`达到`max_turns`,通道以`"max_turns"`为由关闭。(上述`default_target=TerminateTarget("max_iterations")`仅在某一轮无匹配转换时才会触发——在此图中,`FromSpeaker`规则总会匹配,因此实际上是无效的;可保留作为可到达场景的合理回退。)

The eight cookbook patterns

八种Cookbook模式

The AG2 docs ship a Pattern Cookbook with runnable examples. One-line summaries — each one is a
workflow
channel with a specific graph:
PatternGraphWhen
Pipeline
TransitionGraph.sequence([a, b, c])
Linear A→B→C→terminate (research → write → edit).
HierarchicalCoordinator +
FromSpeaker(specialist) → RevertToInitiatorTarget()
Coordinator dispatches; specialist returns to coordinator.
StarHub queries N spokes;
ToolCalled("synthesise") → TerminateTarget
Multi-source aggregation, parallel data gathering.
EscalationTiered
Handoff
+
ToolCalled("resolve") → TerminateTarget("resolved")
Support tiers, multi-level approval.
Redundant
sequence
with N proposers + evaluator
Niche; novelty/ideation with comparison.
Feedback loop
ContextEquals("done", True) → TerminateTarget
+
max_turns
Drafter ↔ reviewer until approved.
Context-aware routingTriage sets
context_vars["category"]
;
ContextEquals(category, X) → AgentTarget(specialist_X)
Support triage, skill-based dispatch.
Triage with tasks
TransitionGraph.sequence
+
knobs["context_vars"]
initial values
Triage produces plan; sequence executes the plan.
Pipeline, escalation, context-aware routing, and feedback loop together cover most real-world needs. The cookbook page has runnable end-to-end code for each.
AG2文档附带了模式手册,包含可运行的示例。一行总结——每种模式都是带有特定图的
workflow
通道:
模式图适用场景
流水线
TransitionGraph.sequence([a, b, c])
线性A→B→C→终止(研究→撰写→编辑)。
分层协调者 +
FromSpeaker(specialist) → RevertToInitiatorTarget()
协调者分配任务;专家返回给协调者。
星型中心查询N个分支;
ToolCalled("synthesise") → TerminateTarget
多源聚合、并行数据收集。
升级分层
Handoff
+
ToolCalled("resolve") → TerminateTarget("resolved")
支持层级、多级审批。
冗余
sequence
包含N个提议者+评估者
小众场景;创意生成与对比。
反馈循环
ContextEquals("done", True) → TerminateTarget
+
max_turns
drafter↔reviewer直到批准。
上下文感知路由分诊设置
context_vars["category"]
;
ContextEquals(category, X) → AgentTarget(specialist_X)
支持分诊、基于技能的分配。
带任务的分诊
TransitionGraph.sequence
+
knobs["context_vars"]
初始值
分诊生成计划;序列执行计划。
流水线、升级、上下文感知路由和反馈循环共同覆盖了大多数实际需求。手册页面包含每种模式的可运行端到端代码。

Custom targets and conditions

自定义目标和条件

When the built-ins don't fit, implement the
Protocol
and register the class so it round-trips through
TransitionGraph.to_dict()
:
python
from typing import ClassVar
from dataclasses import dataclass, field

from ag2.network import (
    Envelope,
    TransitionDecision,
    TransitionTarget,
    register_target,
)


@dataclass(slots=True)
class HighestRankedReviewer(TransitionTarget):
    name: ClassVar[str] = "highest_ranked_reviewer"
    role_priority: list[str] = field(default_factory=list)

    def resolve(self, state, envelope: Envelope) -> TransitionDecision:
        # ... look up the next reviewer based on domain logic ...
        return TransitionDecision(next_speaker=chosen_id)


register_target(HighestRankedReviewer)
Same pattern for conditions — implement
evaluate(state, envelope) -> bool
, set a
ClassVar[str] name
, call
register_condition(MyCondition)
. Both customisations persist via
to_dict()
: the
name
field is the key, dataclass fields become the args dict reconstituted by
loads(...)
.
当内置组件无法满足需求时,实现对应的
Protocol
并注册类,使其可通过
TransitionGraph.to_dict()
进行序列化:
python
from typing import ClassVar
from dataclasses import dataclass, field

from ag2.network import (
    Envelope,
    TransitionDecision,
    TransitionTarget,
    register_target,
)


@dataclass(slots=True)
class HighestRankedReviewer(TransitionTarget):
    name: ClassVar[str] = "highest_ranked_reviewer"
    role_priority: list[str] = field(default_factory=list)

    def resolve(self, state, envelope: Envelope) -> TransitionDecision:
        # ... 根据领域逻辑查找下一位审核员 ...
        return TransitionDecision(next_speaker=chosen_id)


register_target(HighestRankedReviewer)
条件的实现模式相同——实现
evaluate(state, envelope) -> bool
,设置
ClassVar[str] name
,调用
register_condition(MyCondition)
。两种自定义都可通过
to_dict()
持久化:
name
字段是键,数据类字段会成为
loads(...)
重构的参数字典。

The packet execution model and idempotent tools

数据包执行模型与幂等工具

Each
Agent.ask
round on a workflow channel commits to the WAL atomically as a single
EV_PACKET
envelope. The packet carries the agent's routing decision (
routing.tool
matched against
ToolCalled
rules, or a pre-resolved
routing.target
from a typed
Handoff
), the round's body text, and a reserved
context_updates
slot.
EV_CONTEXT_SET
envelopes from tool calls land before the packet, so a
ContextEquals
rule on the same fold sees the just-set value.
Atomicity has a consequence: if the agent crashes mid-packet, the channel reverts to its pre-packet state and the input is re-dispatched. Tool calls in that packet execute again on retry.
Tools that touch external systems must be idempotent under retry:
  • Use the external service's idempotency-key feature where available (Stripe, S3). Derive a stable key from
    (channel_id, round_counter, tool_name)
    .
  • For database writes, use upsert (
    INSERT ... ON CONFLICT
    ) rather than blind insert.
  • For tools that genuinely cannot be idempotent, gate them behind HITL or run them in single-tool rounds so the packet boundary is tighter.
工作流通道上的每个
Agent.ask
轮次都会作为单个
EV_PACKET
信封原子性地提交到WAL。数据包携带Agent的路由决策(
routing.tool
与
ToolCalled
规则匹配,或来自类型化
Handoff
的预解析
routing.target
)、轮次的正文文本,以及保留的
context_updates
槽。工具调用发送的
EV_CONTEXT_SET
信封会在数据包之前处理,因此同一轮次的
ContextEquals
规则会看到刚设置的值。
原子性的影响:若Agent在数据包处理中途崩溃,通道会恢复到数据包处理前的状态,输入会重新分发。该数据包中的工具调用会在重试时再次执行。
涉及外部系统的工具必须在重试时保持幂等:
  • 尽可能使用外部服务的幂等键功能(如Stripe、S3)。从
    (channel_id, round_counter, tool_name)
    派生稳定的键。
  • 数据库写入使用upsert(
    INSERT ... ON CONFLICT
    )而非盲插入。
  • 对于确实无法实现幂等的工具,可将其置于人工介入(HITL)之后,或在单工具轮次中运行,以缩小数据包边界。

Migration from classic GroupChat

从经典GroupChat迁移

The translation is mostly mechanical. The two systems share the vocabulary — speakers, conditions, targets, terminations — but the new one is data-first (a
TransitionGraph
is JSON-serialisable and survives
Hub.hydrate()
).
迁移过程大多是机械性的。两个系统共享词汇——发言者、条件、目标、终止——但新系统是数据优先的(
TransitionGraph
可序列化为JSON,并在
Hub.hydrate()
后保留)。

Concept mapping

概念映射

AG2 ClassicAG2 networkNotes
GroupChat(agents=[...])
workflow
channel with participants
The hub plays
GroupChatManager
's role.
GroupChatManager
WorkflowAdapter
+
Hub
Turn-taking moves into the hub.
Agent.handoffs
TransitionGraph
(per-channel, not per-agent)
Handoffs described once at channel level.
AgentTarget(agent)
AgentTarget(agent_id)
Takes id, not reference.
RevertToInitiator()
RevertToInitiatorTarget()
Same semantics.
Stay()
StayTarget()
Same.
Terminate()
TerminateTarget(reason="…")
Now carries a reason.
OnContextCondition(...)
ContextEquals
or custom
TransitionCondition
Register via
register_condition
.
OnCondition(...)
Custom
TransitionCondition
Same.
ReplyResult(target=...)
from a tool
Typed
Handoff
return
Handoff(target=..., reason=...)
.
FunctionTarget(fn)
Custom
TransitionTarget
Implement
resolve()
,
register_target(...)
.
max_round=N
max_turns=N
on the graph
Same hard cap.
AG2经典模式AG2网络模式说明
GroupChat(agents=[...])
workflow
通道及参与者
枢纽扮演
GroupChatManager
的角色。
GroupChatManager
WorkflowAdapter
+
Hub
轮次控制权转移到枢纽。
Agent.handoffs
TransitionGraph
(每个通道独立配置,而非每个Agent)
切换逻辑在通道级别一次性定义。
AgentTarget(agent)
AgentTarget(agent_id)
接收id,而非引用。
RevertToInitiator()
RevertToInitiatorTarget()
语义相同。
Stay()
StayTarget()
语义相同。
Terminate()
TerminateTarget(reason="…")
现在携带终止原因。
OnContextCondition(...)
ContextEquals
或自定义
TransitionCondition
通过
register_condition
注册。
OnCondition(...)
自定义
TransitionCondition
相同实现方式。
工具返回的
ReplyResult(target=...)
类型化
Handoff
返回
Handoff(target=..., reason=...)
。
FunctionTarget(fn)
自定义
TransitionTarget
实现
resolve()
,调用
register_target(...)
。
max_round=N
图中的
max_turns=N
相同的硬上限。

Side-by-side: round-robin

对比示例:轮询

python
undefined
python
undefined

Classic

经典模式

groupchat = GroupChat(agents=[alice, bob, carol], speaker_selection_method="round_robin", max_round=6) manager = GroupChatManager(groupchat=groupchat, llm_config=llm_config) alice.initiate_chat(manager, message="Topic: …")
groupchat = GroupChat(agents=[alice, bob, carol], speaker_selection_method="round_robin", max_round=6) manager = GroupChatManager(groupchat=groupchat, llm_config=llm_config) alice.initiate_chat(manager, message="Topic: …")

AG2 network

AG2网络模式

graph = TransitionGraph.round_robin( participants=[alice.agent_id, bob.agent_id, carol.agent_id], max_turns=6, ) channel = await alice.open( type="workflow", target=[bob.agent_id, carol.agent_id], knobs={"graph": graph.to_dict()}, ) await channel.send("Topic: …")
undefined
graph = TransitionGraph.round_robin( participants=[alice.agent_id, bob.agent_id, carol.agent_id], max_turns=6, ) channel = await alice.open( type="workflow", target=[bob.agent_id, carol.agent_id], knobs={"graph": graph.to_dict()}, ) await channel.send("Topic: …")
undefined

Side-by-side: conditional handoff (
ReplyResult
→
Handoff
)

对比示例:条件切换(
ReplyResult
→
Handoff
)

python
undefined
python
undefined

Classic

经典模式

@triage.register_for_llm(description="Escalate to security review.") def escalate(reason: str) -> ReplyResult: return ReplyResult(target=AgentTarget(security_reviewer), message=...)
@triage.register_for_llm(description="Escalate to security review.") def escalate(reason: str) -> ReplyResult: return ReplyResult(target=AgentTarget(security_reviewer), message=...)

AG2 network — attach to the Agent before registering with the hub

AG2网络模式——在注册到枢纽前,将工具附加到Agent

@triage_agent.tool async def escalate(reason: str = "") -> Handoff: # Handoff.target is the Passport NAME (resolved name → id by the framework), # whereas AgentTarget in the graph takes the raw agent_id. return Handoff(target="security_reviewer", reason=reason)
@triage_agent.tool async def escalate(reason: str = "") -> Handoff: # Handoff.target是Passport名称(框架会将名称解析为id), # 而图中的AgentTarget接收原始agent_id。 return Handoff(target="security_reviewer", reason=reason)

Plus the matching transition in the graph (as documentation/fallback):

加上图中的匹配转换(作为文档/回退):

Transition(when=ToolCalled("escalate"), then=AgentTarget(security_reviewer.agent_id))
undefined
Transition(when=ToolCalled("escalate"), then=AgentTarget(security_reviewer.agent_id))
undefined

Migration checklist

迁移检查清单

  1. Stand up a hub (
    Hub.open(MemoryKnowledgeStore())
    ).
  2. Wrap each existing
    Agent
    via
    hc.register(...)
    with a
    Passport
    and
    Resume
    .
  3. Translate
    Handoffs
    config into a
    TransitionGraph
    . Use
    sequence
    /
    round_robin
    factories where they fit; build manually for conditional logic.
  4. Replace
    initiate_chat(...)
    with
    alice.open(type="workflow", target=[...], knobs={"graph": graph.to_dict()})
    followed by
    channel.send(text)
    .
  5. Wait for
    EV_CHANNEL_CLOSED
    via
    wait_for_channel_event(...)
    .
  6. Inspect via
    hub.read_wal(channel_id)
    and
    hub._audit_log.read_all()
    .
  1. 启动枢纽(
    Hub.open(MemoryKnowledgeStore())
    )。
  2. 使用
    hc.register(...)
    将每个现有
    Agent
    包装,并提供
    Passport
    和
    Resume
    。
  3. 将
    Handoffs
    配置转换为
    TransitionGraph
    。适用时使用
    sequence
    /
    round_robin
    工厂方法;条件逻辑则手动构建。
  4. 将
    initiate_chat(...)
    替换为
    alice.open(type="workflow", target=[...], knobs={"graph": graph.to_dict()})
    ,然后调用
    channel.send(text)
    。
  5. 通过
    wait_for_channel_event(...)
    等待
    EV_CHANNEL_CLOSED
    事件。
  6. 通过
    hub.read_wal(channel_id)
    和
    hub._audit_log.read_all()
    检查日志。

State object

状态对象

python
@dataclass(slots=True)
class WorkflowState:
    participant_order: list[str]
    expected_next_speaker: str | None
    last_speaker_id: str | None = None
    last_envelope_id: str | None = None
    turn_count: int = 0
    pending_close_reason: str = ""
    creator_id: str = ""
    graph_data: dict = field(default_factory=dict)
    context_vars: dict[str, Any] = field(default_factory=dict)
expected_next_speaker = None
signals "channel should terminate" — the adapter's
on_accepted
reads this and returns
AdapterResult(next_state=CLOSING, ...)
.
graph_data
is the serialised graph; the adapter rebuilds
TransitionGraph
on every fold so there's no mutable graph state in memory.
python
@dataclass(slots=True)
class WorkflowState:
    participant_order: list[str]
    expected_next_speaker: str | None
    last_speaker_id: str | None = None
    last_envelope_id: str | None = None
    turn_count: int = 0
    pending_close_reason: str = ""
    creator_id: str = ""
    graph_data: dict = field(default_factory=dict)
    context_vars: dict[str, Any] = field(default_factory=dict)
expected_next_speaker = None
表示"通道应终止"——适配器的
on_accepted
会读取此值并返回
AdapterResult(next_state=CLOSING, ...)
。
graph_data
是序列化后的图;适配器每轮都会重建
TransitionGraph
,因此内存中不存在可变的图状态。

Quick reference — imports

快速参考——导入

python
from ag2.network import (
    # Graph and transitions
    TransitionGraph,
    Transition,
    TransitionTarget,
    TransitionCondition,
    TransitionDecision,
    register_target,
    register_condition,
    # Built-in targets
    AgentTarget,
    RoundRobinTarget,
    StayTarget,
    RevertToInitiatorTarget,
    TerminateTarget,
    # Built-in conditions
    Always,
    FromSpeaker,
    ToolCalled,
    ContextEquals,
    # Typed handoff return for tools
    Handoff,
    # Context variables
    EV_CONTEXT_SET,
    ChannelInject,
    ChannelStateInject,
    # Channel types
    WORKFLOW_TYPE,
    # Seeding a workflow (kickoff participant)
    HumanClient,  # via HubClient.register_human(Passport(name=..., kind="human"))
)
from ag2.network.workflow_helpers import set_context, delete_context
python
from ag2.network import (
    # 图与转换
    TransitionGraph,
    Transition,
    TransitionTarget,
    TransitionCondition,
    TransitionDecision,
    register_target,
    register_condition,
    # 内置目标
    AgentTarget,
    RoundRobinTarget,
    StayTarget,
    RevertToInitiatorTarget,
    TerminateTarget,
    # 内置条件
    Always,
    FromSpeaker,
    ToolCalled,
    ContextEquals,
    # 工具的类型化切换返回
    Handoff,
    # 上下文变量
    EV_CONTEXT_SET,
    ChannelInject,
    ChannelStateInject,
    # 通道类型
    WORKFLOW_TYPE,
    # 启动工作流(初始参与者)
    HumanClient,  # 通过HubClient.register_human(Passport(name=..., kind="human"))获取
)
from ag2.network.workflow_helpers import set_context, delete_context