ag2-network-workflow
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAG2 Network — Workflow Adapter
AG2 网络 — 工作流适配器
workflowTransitionGraphGroupChat + Agent.handoffsPrerequisite: readfirst. This skill assumes you knowag2-network-quickstart,Hub.open, the channel lifecycle, and the basicHubClient.register/agent_client.open(...)flow.channel.send(...)
workflowTransitionGraphGroupChat + 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 for:
workflow- Strict 1Q1R between two agents → (see
consulting).ag2-network-quickstart - Free-form 2-party chat with no turn order → (see
conversation).ag2-network-quickstart - Fixed round-robin with no conditions → (see
discussion).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
若用户不确定,可遵循经验法则:需要比「固定列表中的下一位发言者」更复杂的条件?就用。
workflowShape
特性参数
| Participants | 2+ |
| Turn order | Whatever |
| Auto-close | Yes — when graph emits |
| Default view | |
| Default expectations | |
| Required knob | |
The expectation is strict: a stuck speaker auto-closes the channel after 10 minutes. Tune via .
ag2-network-governance| 参与者 | 2+ |
| 轮次顺序 | 由 |
| 自动关闭 | 是——当图输出 |
| 默认视图 | |
| 默认规则 | |
| 必填配置 | |
规则严格:发言者停滞10分钟后,通道将自动关闭。可通过调整。
ag2-network-governanceWorkflow agents and the NetworkPlugin
NetworkPlugin工作流Agent与NetworkPlugin
NetworkPluginHubClient.register(...)NetworkPluginpeerschannelstaskscontextdelegatesay@tooladapter.tools_for(client, metadata, state, participant_id)agent.ask(tools=...)workflowWorkflowAdapter.tools_for(...)[]say@toolHandoffchannel.close()Consequence: the old "mid-turn races the round-end " hazard can't happen on a workflow channel — you do not need for correctness. Choose by capability instead:
sayEV_PACKETattach_plugin=False- Keep the plugin (default) if the agent should also to a peer, look up
delegate/peers/channels, or read/write channeltasksfrom outside the graph. (context/delegatespin up separate channels — they never emit on the workflow channel, so they don't disturb its turn machinery.)channels(action="open") - for a genuinely bare agent — only the domain
attach_plugin=Falses you wrote. Tidy for pure pipeline workers and most tests.@tool
python
undefinedHubClient.register(...)NetworkPluginpeerschannelstaskscontextdelegatesay@tooladapter.tools_for(client, metadata, state, participant_id)agent.ask(tools=...)workflowWorkflowAdapter.tools_for(...)[]say@toolHandoffchannel.close()由此带来的好处:旧模式中「轮次中途与轮次结束竞争」的风险在workflow通道中完全不会发生——你无需设置来保证正确性。可根据功能需求选择:
sayEV_PACKETattach_plugin=False- 保留插件(默认):若Agent需要向对等方、查询
delegate/peers/channels,或从外部读取/写入通道tasks。(context/delegate会启动独立通道——它们不会在workflow通道上输出内容,因此不会干扰轮次机制。)channels(action="open") - 设置:用于纯流水线工作者和大部分测试场景,仅保留你编写的领域
attach_plugin=Falses。@tool
python
undefinedBare 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最小示例——通过sequence
实现流水线
sequencepython
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")cpython
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")cTransitionGraph
anatomy
TransitionGraphTransitionGraph
结构
TransitionGraphpython
@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 orderOn every accepted substantive envelope (text or packet), the adapter walks the list. The first matching condition's target resolves the next speaker. If none match, is consulted. A resolves with , which makes the adapter return and the hub posts with that target's . is a separate, harder cap: once reaches it the adapter closes the channel immediately with reason — it does not fall through to (so a reason only appears when an actual turn produced no matching transition).
transitionsdefault_targetTerminateTargetnext_speaker=NoneAdapterResult(next_state=CLOSING, ...)EV_CHANNEL_CLOSEDreasonmax_turnsturn_count"max_turns"default_targetdefault_target=TerminateTarget("…")prioritysorted(transitions, key=lambda t: t.priority)priority=0priority=100priority=100ContextEquals(...) → TerminateTarget(...)FromSpeaker(...)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 # 数值越小优先级越高;优先级相同时按插入顺序处理每接收一个有效信封(文本或数据包),适配器都会遍历列表。第一个匹配条件的目标将解析下一位发言者。若无匹配,则使用。会返回,使适配器返回,同时枢纽会发布事件,并携带该目标的。是独立的硬上限:一旦达到该值,适配器会立即关闭通道,原因是****——不会回退到(因此的原因仅在某一轮无匹配转换时才会出现)。
transitionsdefault_targetTerminateTargetnext_speaker=NoneAdapterResult(next_state=CLOSING, ...)EV_CHANNEL_CLOSEDreasonmax_turnsturn_count"max_turns"default_targetdefault_target=TerminateTarget("…")prioritysorted(transitions, key=lambda t: t.priority)priority=0priority=100ContextEquals(...) → TerminateTarget(...)priority=100FromSpeaker(...)Gotcha — the kickoff channel.send()
is the initial_speaker
's first turn
channel.send()initial_speaker注意事项——启动时的channel.send()
是initial_speaker
的第一轮发言
channel.send()initial_speakerWhen you do then , that first send is folded as 's turn — it's an envelope from that agent, and starts equal to , so accepts it and the graph immediately routes onward. The 's never runs for that turn — the text you sent is its contribution.
agent.open(type="workflow", ...)await channel.send(text)initial_speakerexpected_next_speakerinitial_speakervalidate_sendinitial_speakerAgent.askSo 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 and must not be the one calling . The clean answer is a as the seeder — a non-LLM participant (no , no plugin) whose only job is to open the channel and post the brief. Make it and route :
initial_speakerchannel.send()HumanClientAgentinitial_speakerFromSpeaker(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发送的信封,且初始值等于,因此会接受它,图会立即路由到下一位发言者。的在这一轮不会运行——你发送的文本就是它的贡献内容。
agent.open(type="workflow", ...)await channel.send(text)initial_speakerexpected_next_speakerinitial_speakervalidate_sendinitial_speakerAgent.ask因此,若你的设计希望实际执行思考的第一个Agent(起草人、研究员、规划师)响应启动提示,该Agent不能是,也不能是调用的对象。最佳方案是使用****作为启动者——一个非LLM参与者(无、无插件),仅负责打开通道并发布提示。将其设为,并设置路由规则:
initial_speakerchannel.send()HumanClientAgentinitial_speakerFromSpeaker(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
内置目标
| Target | Decision |
|---|---|
| Hand off to a specific named agent. |
| Advance through the participant order. |
| Same speaker continues (rare; "let me elaborate" patterns). |
| Hand back to whoever opened the channel. |
| End the channel; reason flows on |
| 目标 | 决策 |
|---|---|
| 切换到指定的Agent。 |
| 按参与者顺序推进。 |
| 同一发言者继续(罕见;用于"让我详细说明"等场景)。 |
| 切换回打开通道的参与者。 |
| 结束通道;原因会携带在 |
Built-in conditions
内置条件
| Condition | Fires when |
|---|---|
| Every accepted envelope. |
| The just-accepted envelope was sent by this agent. |
| The previous turn called this tool by name (matched via |
| Channel-scoped |
For combinations beyond what these compose to, see "Custom targets / conditions" below.
| 条件 | 触发时机 |
|---|---|
| 每接收一个有效信封时。 |
| 刚接收的信封由该Agent发送。 |
| 上一轮调用了指定名称的工具(通过数据包中的 |
| 通道级 |
若需要这些内置条件无法组合实现的逻辑,见下文"自定义目标/条件"。
Convenience factories
便捷工厂方法
python
undefinedpython
undefinedPipeline: 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 transition, attach an -decorated function on the speaker's . The simplest form returns a typed — the framework reads it from the tool's result and routes:
ToolCalled(name) → AgentTarget(agent)@toolAgentHandoffpython
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@toolHandoffpython
from ag2.network import Handoff.tool
lives on Agent
. The AgentClient
returned by hc.register(...)
.toolAgentAgentClienthc.register(...).tool
属于Agent
。hc.register(...)
返回的AgentClient
是网络句柄(用于.open()
、.send()
、.agent_id
),
.toolAgenthc.register(...)AgentClient.open().send().agent_idis the network handle (used for .open()
, .send()
, .agent_id
) and does
.open().send().agent_id不暴露.tool
。因此:先构建Agent,附加工具,再注册。
.toolNOT expose .tool
. So: build the Agent, attach tools, then register.
.tool—
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 , scoped to one workflow channel and persisted as envelopes on the WAL.
WorkflowState.context_vars: dict[str, Any]EV_CONTEXT_SET通道级可变状态存储在中,作用域为单个工作流通道,并作为信封持久化到WAL中。
WorkflowState.context_vars: dict[str, Any]EV_CONTEXT_SETWriting 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)EV_CONTEXT_SETaudience=NoneWorkflowState.context_varspython
await channel.send(
"",
event_type=EV_CONTEXT_SET,
event_data={"set": {"route": route}},
)The full event_data shape supports both set and delete: . Either is optional; within one envelope runs before .
{"set": {...}, "delete": [...]}deletesetpython
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_SETaudience=NoneWorkflowState.context_varspython
await channel.send(
"",
event_type=EV_CONTEXT_SET,
event_data={"set": {"route": route}},
)完整的event_data格式支持设置和删除:。两者都是可选的;在同一个信封中,会先于执行。
{"set": {...}, "delete": [...]}deletesetReading context (in a transition)
读取上下文(在转换中)
python
Transition(
when=ContextEquals(key="route", value="security"),
then=AgentTarget(security.agent_id),
)ContextEquals(key, value=None)ContextEqualspython
Transition(
when=ContextEquals(key="route", value="security"),
then=AgentTarget(security.agent_id),
)当键未设置或被显式删除时,会触发。变更会在实质性轮次检查之前被折叠,因此同一轮次的规则会看到刚设置的值。
ContextEquals(key, value=None)ContextEqualsReading 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
路由循环陷阱
ContextEqualsContextEquals("route", "security") → AgentTarget(security)max_turns- List terminate rules before context-conditions: placed earlier short-circuits the loop after security speaks.
FromSpeaker(security) → TerminateTarget(...) - Have the routed agent clear the key: the security agent's tool emits with
EV_CONTEXT_SETwhen done.{"delete": ["route"]}
Fix #1 is the more common pattern.
ContextEqualsContextEquals("route", "security") → AgentTarget(security)max_turns- 将终止规则放在上下文条件之前:放在前面,可在安全专家发言后终止循环。
FromSpeaker(security) → TerminateTarget(...) - 让被路由的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_agentAgentreviewerAgentClientreviewer_agent
是Agent
;reviewer
是register()
返回的AgentClient
,用于上述图中的reviewer.agent_id
。
reviewer_agentAgentreviewerregister()AgentClientreviewer.agent_idby register()
and used in the graph above (for reviewer.agent_id
).
register()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 channel with a specific graph:
workflow| Pattern | Graph | When |
|---|---|---|
| Pipeline | | Linear A→B→C→terminate (research → write → edit). |
| Hierarchical | Coordinator + | Coordinator dispatches; specialist returns to coordinator. |
| Star | Hub queries N spokes; | Multi-source aggregation, parallel data gathering. |
| Escalation | Tiered | Support tiers, multi-level approval. |
| Redundant | | Niche; novelty/ideation with comparison. |
| Feedback loop | | Drafter ↔ reviewer until approved. |
| Context-aware routing | Triage sets | Support triage, skill-based dispatch. |
| Triage with tasks | | 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| 模式 | 图 | 适用场景 |
|---|---|---|
| 流水线 | | 线性A→B→C→终止(研究→撰写→编辑)。 |
| 分层 | 协调者 + | 协调者分配任务;专家返回给协调者。 |
| 星型 | 中心查询N个分支; | 多源聚合、并行数据收集。 |
| 升级 | 分层 | 支持层级、多级审批。 |
| 冗余 | | 小众场景;创意生成与对比。 |
| 反馈循环 | | drafter↔reviewer直到批准。 |
| 上下文感知路由 | 分诊设置 | 支持分诊、基于技能的分配。 |
| 带任务的分诊 | | 分诊生成计划;序列执行计划。 |
流水线、升级、上下文感知路由和反馈循环共同覆盖了大多数实际需求。手册页面包含每种模式的可运行端到端代码。
Custom targets and conditions
自定义目标和条件
When the built-ins don't fit, implement the and register the class so it round-trips through :
ProtocolTransitionGraph.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 , set a , call . Both customisations persist via : the field is the key, dataclass fields become the args dict reconstituted by .
evaluate(state, envelope) -> boolClassVar[str] nameregister_condition(MyCondition)to_dict()nameloads(...)当内置组件无法满足需求时,实现对应的并注册类,使其可通过进行序列化:
ProtocolTransitionGraph.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) -> boolClassVar[str] nameregister_condition(MyCondition)to_dict()nameloads(...)The packet execution model and idempotent tools
数据包执行模型与幂等工具
Each round on a workflow channel commits to the WAL atomically as a single envelope. The packet carries the agent's routing decision ( matched against rules, or a pre-resolved from a typed ), the round's body text, and a reserved slot. envelopes from tool calls land before the packet, so a rule on the same fold sees the just-set value.
Agent.askEV_PACKETrouting.toolToolCalledrouting.targetHandoffcontext_updatesEV_CONTEXT_SETContextEqualsAtomicity 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 () rather than blind insert.
INSERT ... ON CONFLICT - For tools that genuinely cannot be idempotent, gate them behind HITL or run them in single-tool rounds so the packet boundary is tighter.
工作流通道上的每个轮次都会作为单个信封原子性地提交到WAL。数据包携带Agent的路由决策(与规则匹配,或来自类型化的预解析)、轮次的正文文本,以及保留的槽。工具调用发送的信封会在数据包之前处理,因此同一轮次的规则会看到刚设置的值。
Agent.askEV_PACKETrouting.toolToolCalledHandoffrouting.targetcontext_updatesEV_CONTEXT_SETContextEquals原子性的影响:若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 is JSON-serialisable and survives ).
TransitionGraphHub.hydrate()迁移过程大多是机械性的。两个系统共享词汇——发言者、条件、目标、终止——但新系统是数据优先的(可序列化为JSON,并在后保留)。
TransitionGraphHub.hydrate()Concept mapping
概念映射
| AG2 Classic | AG2 network | Notes |
|---|---|---|
| | The hub plays |
| | Turn-taking moves into the hub. |
| | Handoffs described once at channel level. |
| | Takes id, not reference. |
| | Same semantics. |
| | Same. |
| | Now carries a reason. |
| | Register via |
| Custom | Same. |
| Typed | |
| Custom | Implement |
| | Same hard cap. |
| AG2经典模式 | AG2网络模式 | 说明 |
|---|---|---|
| | 枢纽扮演 |
| | 轮次控制权转移到枢纽。 |
| | 切换逻辑在通道级别一次性定义。 |
| | 接收id,而非引用。 |
| | 语义相同。 |
| | 语义相同。 |
| | 现在携带终止原因。 |
| | 通过 |
| 自定义 | 相同实现方式。 |
工具返回的 | 类型化 | |
| 自定义 | 实现 |
| 图中的 | 相同的硬上限。 |
Side-by-side: round-robin
对比示例:轮询
python
undefinedpython
undefinedClassic
经典模式
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: …")
undefinedgraph = 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: …")
undefinedSide-by-side: conditional handoff (ReplyResult
→ Handoff
)
ReplyResultHandoff对比示例:条件切换(ReplyResult
→ Handoff
)
ReplyResultHandoffpython
undefinedpython
undefinedClassic
经典模式
@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))
undefinedTransition(when=ToolCalled("escalate"), then=AgentTarget(security_reviewer.agent_id))
undefinedMigration checklist
迁移检查清单
- Stand up a hub ().
Hub.open(MemoryKnowledgeStore()) - Wrap each existing via
Agentwith ahc.register(...)andPassport.Resume - Translate config into a
Handoffs. UseTransitionGraph/sequencefactories where they fit; build manually for conditional logic.round_robin - Replace with
initiate_chat(...)followed byalice.open(type="workflow", target=[...], knobs={"graph": graph.to_dict()}).channel.send(text) - Wait for via
EV_CHANNEL_CLOSED.wait_for_channel_event(...) - Inspect via and
hub.read_wal(channel_id).hub._audit_log.read_all()
- 启动枢纽()。
Hub.open(MemoryKnowledgeStore()) - 使用将每个现有
hc.register(...)包装,并提供Agent和Passport。Resume - 将配置转换为
Handoffs。适用时使用TransitionGraph/sequence工厂方法;条件逻辑则手动构建。round_robin - 将替换为
initiate_chat(...),然后调用alice.open(type="workflow", target=[...], knobs={"graph": graph.to_dict()})。channel.send(text) - 通过等待
wait_for_channel_event(...)事件。EV_CHANNEL_CLOSED - 通过和
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 = Noneon_acceptedAdapterResult(next_state=CLOSING, ...)graph_dataTransitionGraphpython
@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 = Noneon_acceptedAdapterResult(next_state=CLOSING, ...)graph_dataTransitionGraphQuick 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_contextpython
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