ag2-network-governance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAG2 Network — Governance
AG2 网络——治理
Everything hub-side: identity, per-agent rules, expectations, audit, and task observation. The hub is the single source of truth — every send goes through it, every observation reads from it, every policy is checked there.
Prerequisite: readfirst. This skill assumes you knowag2-network-quickstart,Hub.open,Passport, the channel lifecycle, and theResumeflow.agent_client.register(...)
所有中心节点侧的功能:身份标识、每个智能体的规则、预期配置、审计以及任务观测。中心节点是唯一的可信数据源——所有消息发送都需经过它,所有观测数据都从它读取,所有策略都在它这里校验。
前置要求:请先阅读。本技能默认你已了解ag2-network-quickstart、Hub.open、Passport、通道生命周期以及Resume流程。agent_client.register(...)
When to use
使用场景
Load this skill when the user needs to:
- Limit who can talk to whom (/
access)AccessBlock - Declare a token-bucket rate intent (/
limits.rate) — stored on the rule but not enforced by the in-process hubRateBlock - Cap inbox size to prevent flooding (/
limits.inbox) — and get an early-warning signal before the cap (InboxBlock/on_inbox_pressure)high_water - Set channel TTL defaults, concurrency caps, or delegation depth (/
limits)LimitsBlock - Plug in custom access / routing logic — JWT scopes, per-tenant quotas, federation (/
HubArbiter)RuleBasedArbiter - Authenticate agents at registration ()
AuthAdapter - Tune the channel-close timing (,
acks_within,reply_within,max_silence)turn_within - Read or query the audit log for compliance — or stream live state changes to metrics / alerting ()
HubListener - Add a custom periodic task to the hub ()
register_sweeper - Build a capability track record on each agent ()
Resume.observed - Route based on which agents have demonstrably done a task (e.g. "send to whichever researcher has the lowest ")
p50_latency_ms
当用户需要以下功能时,加载本技能:
- 限制智能体之间的通信对象(/
access)AccessBlock - 声明令牌桶速率限制意图(/
limits.rate)——规则中会存储该配置,但进程内中心节点不强制执行RateBlock - 限制收件箱大小以防止消息泛滥(/
limits.inbox)——并在达到上限前触发预警信号(InboxBlock/on_inbox_pressure)high_water - 设置通道TTL默认值、并发上限或委托深度(/
limits)LimitsBlock - 接入自定义访问/路由逻辑——JWT权限范围、租户配额、联邦机制(/
HubArbiter)RuleBasedArbiter - 在注册时验证智能体身份()
AuthAdapter - 调整通道关闭时机(、
acks_within、reply_within、max_silence)turn_within - 读取或查询审计日志以满足合规要求——或实时流式传输状态变更至指标/告警系统()
HubListener - 为中心节点添加自定义定时任务()
register_sweeper - 为每个智能体构建能力记录()
Resume.observed - 根据智能体已完成任务的情况进行路由(例如:"发送给最低的研究员")
p50_latency_ms
Identity — what every agent carries
身份标识——每个智能体的必备信息
Three dataclasses describe an agent on the network. The tenant supplies most fields; the hub stamps the rest.
python
from ag2.network import Passport, Resume, ResumeExample
passport = Passport(
name="alice", # required, unique within the hub
owner="acme", # optional, tenant id for multi-tenant deployments
model="claude-sonnet-4-6", # optional, surfaces on peer-lookup results
)
resume = Resume(
claimed_capabilities=["analysis", "policy"],
domains=["finance"],
summary="Senior policy analyst — scenario synthesis and rebuttal review.",
examples=[ResumeExample(title="Q3 risk brief", note="…")],
)| Field | Source |
|---|---|
| tenant (required, unique) |
| hub-stamped at registration; use for routing |
| hub-stamped ISO-Z timestamp |
| tenant (free-form strings: |
| tenant — indexed for peer lookup |
| hub-mutated per-capability |
| hub-stamped ISO-Z, refreshed on mutation |
The field is the agent's track record. It grows automatically as the agent runs capability-tagged tasks (see "Task observation" below).
observed三个数据类描述了网络中的智能体。大部分字段由租户提供,其余由中心节点标记。
python
from ag2.network import Passport, Resume, ResumeExample
passport = Passport(
name="alice", # 必填,中心节点内唯一
owner="acme", # 可选,多租户部署中的租户ID
model="claude-sonnet-4-6", # 可选,在peer查询结果中展示
)
resume = Resume(
claimed_capabilities=["analysis", "policy"],
domains=["finance"],
summary="Senior policy analyst — scenario synthesis and rebuttal review.",
examples=[ResumeExample(title="Q3 risk brief", note="…")],
)| 字段 | 来源 |
|---|---|
| 租户(必填,唯一) |
| 中心节点标记:注册时生成;用于路由 |
| 中心节点标记:ISO-Z时间戳 |
| 租户(自由格式字符串: |
| 租户——用于peer查询的索引字段 |
| 中心节点更新:每个能力对应的 |
| 中心节点标记:ISO-Z时间戳,变更时刷新 |
observedPer-agent rules
每个智能体的规则
Pass a at registration to govern an agent's behaviour on the network:
Rulepython
from ag2.network import (
Rule, AccessBlock, LimitsBlock, RateBlock, InboxBlock, ChannelTypeAccess,
)
rule = Rule(
access=AccessBlock(
outbound_to=["bob", "carol"], # whitelist of recipients (names or ids)
channel_types=ChannelTypeAccess(
initiate=["consulting", "discussion"],
accept=["consulting", "discussion"],
),
),
limits=LimitsBlock( # rate + inbox nest INSIDE limits
channel_ttl_default="4h", # default TTL for channels this agent creates
delegation_depth=2, # max recursion through sub-task delegation
rate=RateBlock(per_minute=60, burst=10),
inbox=InboxBlock(max_pending=100), # cap inbound queue depth
),
)
alice = await alice_hc.register(
Agent("alice", config=config),
Passport(name="alice"),
Resume(),
rule=rule,
)A has two top-level blocks: () and (). and nest inside (as and ).
RuleaccessAccessBlocklimitsLimitsBlockRateBlockInboxBlocklimitslimits.ratelimits.inbox| Block | Lives at | Controls | Failure mode |
|---|---|---|---|
| | Who this agent can address ( | |
| | TTL defaults; | |
| | Token-bucket values ( | Stored but not enforced in-process ( |
| | Inbound queue depth ( | |
When a rule check fails the hub raises the matching error from or ; the envelope never lands on the WAL. Rule changes are audited (kind via ), but a send denial is not written to the built-in audit log — it surfaces via the listener fan-out (the built-in implements no handler). Register a if you need to capture rejections. The component that runs those checks — and the seam where you'd plug in something other than rule data — is the arbiter, below.
channel.send(...)hc.register(...)AUDIT_KIND_RULE_SETset_ruleon_envelope_rejectedAuditLogon_envelope_rejectedHubListener注册时传入以管控智能体在网络中的行为:
Rulepython
from ag2.network import (
Rule, AccessBlock, LimitsBlock, RateBlock, InboxBlock, ChannelTypeAccess,
)
rule = Rule(
access=AccessBlock(
outbound_to=["bob", "carol"], # 允许发送的收件人白名单(名称或ID)
channel_types=ChannelTypeAccess(
initiate=["consulting", "discussion"],
accept=["consulting", "discussion"],
),
),
limits=LimitsBlock( # rate和inbox嵌套在limits内部
channel_ttl_default="4h", # 该智能体创建的通道默认TTL
delegation_depth=2, # 子任务委托的最大递归深度
rate=RateBlock(per_minute=60, burst=10),
inbox=InboxBlock(max_pending=100), # 入站队列深度上限
),
)
alice = await alice_hc.register(
Agent("alice", config=config),
Passport(name="alice"),
Resume(),
rule=rule,
)RuleaccessAccessBlocklimitsLimitsBlockRateBlockInboxBlocklimitslimits.ratelimits.inbox| 模块 | 位置 | 管控内容 | 失败模式 |
|---|---|---|---|
| | 该智能体可发送的对象( | |
| | TTL默认值; | |
| | 令牌桶参数( | 仅存储但进程内不强制执行(默认 |
| | 入站队列深度( | 向发送方返回 |
当规则校验失败时,中心节点会从或抛出对应的错误;消息信封不会写入WAL。规则变更会被审计(通过触发),但发送被拒绝的情况不会写入内置审计日志——而是通过监听器广播(内置未实现处理程序)。如果需要捕获拒绝事件,请注册。执行这些校验的组件——以及你可以接入自定义逻辑的接口——是下面的仲裁器(arbiter)。
channel.send(...)hc.register(...)set_ruleAUDIT_KIND_RULE_SETon_envelope_rejectedAuditLogon_envelope_rejectedHubListenerUpdating a rule after registration
注册后更新规则
python
new_rule = Rule(access=AccessBlock(outbound_to=["bob"]))
await hub.set_rule(alice.agent_id, new_rule) # emits AUDIT_KIND_RULE_SETpython
new_rule = Rule(access=AccessBlock(outbound_to=["bob"]))
await hub.set_rule(alice.agent_id, new_rule) # 触发AUDIT_KIND_RULE_SETParsing duration strings
解析时长字符串
LimitsBlock.channel_ttl_defaultparse_durationpython
from ag2.network import parse_duration
parse_duration("30s") # 30
parse_duration("4h") # 14400
parse_duration("2d") # 172800smhdint0intLimitsBlock.channel_ttl_defaultparse_durationpython
from ag2.network import parse_duration
parse_duration("30s") # 30
parse_duration("4h") # 14400
parse_duration("2d") # 172800支持、、、后缀;纯整数(或类型)会被视为秒数;空字符串返回。返回值为秒数的整数。
smhdint0The arbiter — swappable access & routing
仲裁器——可替换的访问与路由
The hub doesn't enforce s with inline checks anymore; it delegates every access / routing decision to a — a Protocol with one method per decision point, each returning or :
RuleifHubArbiterAllow()Deny(reason, error=<NetworkError subclass>)| Method | Consulted before… | Default |
|---|---|---|
| committing a registration | |
| creating a channel (invitee | |
| appending an envelope to the WAL (outbound access + delegation depth) | |
| enqueuing into a recipient's inbox (capacity) | |
| dispatching one delivery | |
| dispatching to ids the hub doesn't know — returns | — |
The default is — it enforces the per-agent : (outbound/inbound name globs, channel types) plus the caps it actually checks (, , ). is not enforced. This is exactly what the hub did inline before this seam existed. If you only use , you never touch the arbiter.
RuleBasedArbiterRuleaccesslimitsdelegation_depthmax_concurrent_channelsinbox.max_pendinglimits.rateRuleSwap it to layer your own logic — JWT scopes, per-tenant quotas, federation routing — on top of (or instead of) the rule data. returns for everything, so a subclass that overrides one gate would allow the rest — to keep rule enforcement, delegate to a instance:
BaseHubArbiterAllow()RuleBasedArbiter()python
from ag2.network import HubArbiter, BaseHubArbiter, RuleBasedArbiter, Allow, Deny
class ScopedArbiter(BaseHubArbiter):
def __init__(self, inner: HubArbiter) -> None:
self._inner = inner # the rule checks
async def authorize_send(self, envelope, sender, sender_rule, recipients):
if not _token_has_scope(sender, "net.send"):
return Deny("missing net.send scope") # → AccessDeniedError back to the caller
return await self._inner.authorize_send(envelope, sender, sender_rule, recipients)
# authorize_register / _channel_open / _inbox / _dispatch / resolve_unknown_audience
# all fall through to BaseHubArbiter's Allow() — explicitly re-delegate any you want enforced.
hub.register_arbiter(ScopedArbiter(RuleBasedArbiter())) # one active arbiter; replaces the prior one中心节点不再通过内联校验来强制执行;而是将所有访问/路由决策委托给****——一个协议,每个决策点对应一个方法,返回或:
ifRuleHubArbiterAllow()Deny(reason, error=<NetworkError子类>)| 方法 | 触发时机 | 默认拒绝错误 |
|---|---|---|
| 提交注册前 | |
| 创建通道前(校验受邀方 | |
| 将信封写入WAL前(校验出站访问权限 + 委托深度) | |
| 将信封加入收件人队列前(校验容量) | |
| 分发单个消息前 | |
| 向中心节点未知的ID分发消息时——返回 | — |
默认实现是****——它强制执行每个智能体的:(出站/入站名称通配符、通道类型)以及中实际校验的上限(、、)。不强制执行。这与中心节点之前内联执行的逻辑完全一致。如果仅使用,则无需接触仲裁器。
RuleBasedArbiterRuleaccesslimitsdelegation_depthmax_concurrent_channelsinbox.max_pendinglimits.rateRule替换仲裁器以叠加自定义逻辑——JWT权限范围、租户配额、联邦路由——在规则数据之上(或替代规则数据)。对所有请求返回,因此仅覆盖某个校验点的子类会允许其他所有请求——若要保留规则校验,请委托给实例:
BaseHubArbiterAllow()RuleBasedArbiter()python
from ag2.network import HubArbiter, BaseHubArbiter, RuleBasedArbiter, Allow, Deny
class ScopedArbiter(BaseHubArbiter):
def __init__(self, inner: HubArbiter) -> None:
self._inner = inner # 规则校验逻辑
async def authorize_send(self, envelope, sender, sender_rule, recipients):
if not _token_has_scope(sender, "net.send"):
return Deny("missing net.send scope") # → 返回AccessDeniedError给调用方
return await self._inner.authorize_send(envelope, sender, sender_rule, recipients)
# authorize_register / _channel_open / _inbox / _dispatch / resolve_unknown_audience
# 均默认使用BaseHubArbiter的Allow()——如需强制执行,请显式委托。
hub.register_arbiter(ScopedArbiter(RuleBasedArbiter())) # 仅一个活跃仲裁器;替换之前的实例hub.arbiter → the active instance (read-only; handy in tests)
hub.arbiter → 活跃实例(只读;测试时常用)
`Deny.error` picks the `NetworkError` subclass the hub raises (default `AccessDeniedError`) — `Deny(..., error=InboxFull)` etc. to control it. The arbiter is the **gatekeeper** (consulted *before* the state change); `HubListener` (later) is the **observer** (notified *after*). It's a different concern from `AuthAdapter` below — that authenticates *credentials* once at registration; the arbiter authorizes *actions* throughout the channel's life.
`Deny.error`指定中心节点抛出的`NetworkError`子类(默认`AccessDeniedError`)——使用`Deny(..., error=InboxFull)`等可控制错误类型。仲裁器是** gatekeeper**(状态变更前校验);`HubListener`(下文)是** observer**(状态变更后通知)。这与下文的`AuthAdapter`是不同的关注点——`AuthAdapter`在注册时一次性验证**凭证**;仲裁器在通道生命周期内持续授权**操作**。Authentication
身份验证
By default the hub uses — a -only registry (scheme ) that accepts every claim, so every registration succeeds without credentials. The scheme is selected per-passport via (an field, defaulting to ); the credentials live in (a ). For production:
AuthRegistry.default()NoAuth"none"passport.auth.schemeAuthBlock"none"passport.auth.claimdictpython
from ag2.network import AuthAdapter, AuthRegistry, NoAuth, ApiKeyAuth, AuthError, Hub
from ag2.network import AuthBlock, Passport
from ag2.knowledge import MemoryKnowledgeStore
import hmac
from typing import Any
class HMACAuth:
scheme = "hmac" # class-level scheme label
async def validate(self, passport: Passport, claim: dict[str, Any]) -> None:
expected = self._sign(passport.name)
token = claim.get("token", "")
if not hmac.compare_digest(expected, token):
raise AuthError(f"bad hmac for {passport.name}")默认情况下,中心节点使用——仅包含的注册器(方案为),接受所有声明,因此所有注册无需凭证即可成功。每个护照通过(字段,默认)选择验证方案;凭证存储在(字典)中。生产环境使用示例:
AuthRegistry.default()NoAuth"none"passport.auth.schemeAuthBlock"none"passport.auth.claimpython
from ag2.network import AuthAdapter, AuthRegistry, NoAuth, ApiKeyAuth, AuthError, Hub
from ag2.network import AuthBlock, Passport
from ag2.knowledge import MemoryKnowledgeStore
import hmac
from typing import Any
class HMACAuth:
scheme = "hmac" # 类级别的方案标签
async def validate(self, passport: Passport, claim: dict[str, Any]) -> None:
expected = self._sign(passport.name)
token = claim.get("token", "")
if not hmac.compare_digest(expected, token):
raise AuthError(f"bad hmac for {passport.name}")AuthRegistry takes a LIST of adapters at construction (keyed by .scheme).
AuthRegistry在构造时接受适配器列表(按.scheme键区分)。
registry = AuthRegistry([NoAuth(), HMACAuth()])
hub = await Hub.open(MemoryKnowledgeStore(), auth=registry)
`AuthAdapter` is a `Protocol` with a `scheme` attribute and a `validate` method:
```python
class AuthAdapter(Protocol):
scheme: str
async def validate(self, passport: Passport, claim: dict[str, Any]) -> None: ...Raise to reject. At registration the hub looks up the adapter by , calls , and records on success. Remote-agent passports skip the local auth check. The library ships (accept-all, the default) and (constant-time token compare against ).
AuthErrorpassport.auth.schemeadapter.validate(passport, passport.auth.claim)AUDIT_KIND_AGENT_REGISTEREDNoAuthApiKeyAuth(keys=..., resolver=...)claim["token"]registry = AuthRegistry([NoAuth(), HMACAuth()])
hub = await Hub.open(MemoryKnowledgeStore(), auth=registry)
`AuthAdapter`是一个协议,包含`scheme`属性和`validate`方法:
```python
class AuthAdapter(Protocol):
scheme: str
async def validate(self, passport: Passport, claim: dict[str, Any]) -> None: ...抛出表示拒绝。注册时,中心节点根据查找适配器,调用,成功则记录。远程智能体的护照跳过本地身份验证。库内置了(接受所有请求,默认)和(对执行常量时间令牌比对)。
AuthErrorpassport.auth.schemeadapter.validate(passport, passport.auth.claim)AUDIT_KIND_AGENT_REGISTEREDNoAuthApiKeyAuth(keys=..., resolver=...)claim["token"]Expectations — channel-level SLAs
预期配置——通道级SLA
Every adapter ships defaults in its manifest. The expectation sweeper task evaluates them every (default 10s) and dispatches violations to handlers.
expectation_sweep_interval每个适配器在其清单中提供默认配置。预期清理任务每隔(默认10秒)评估一次,并将违规情况分发给处理程序。
expectation_sweep_intervalBuilt-in evaluators
内置评估器
default_evaluators()| Name | Class | Default | Threshold |
|---|---|---|---|
| | 30 | All still-pending invitees must ack within |
| | 600 | A participant addressed by an |
| | 3600 | The channel has no content envelope from anyone for |
Theanddiscussionadapters declareworkflowexpectations on their manifests, but no built-inturn_withinevaluator ships — andturn_within/"warn"are not built-in handlers. The sweeper silently skips any expectation whose"hide"has no registered evaluator or whosenamehas no registered handler (on_violationdoes_expectation_tickand.get(...)s oncontinue). To makeNone/turn_within/warnactive, register your own evaluator (hide) and handler (register_expectation_evaluator).register_violation_handler
default_evaluators()| 名称 | 类 | 默认秒数 | 阈值规则 |
|---|---|---|---|
| | 30 | 所有待确认的受邀方必须在通道创建后的 |
| | 600 | 被 |
| | 3600 | 通道在 |
和discussion适配器在其清单中声明了workflow预期配置,但没有内置的turn_within评估器——且turn_within/"warn"也不是内置处理程序。清理任务会静默跳过任何"hide"未注册评估器或name未注册处理程序的预期配置(on_violation会调用_expectation_tick,若返回.get(...)则继续执行)。要启用None/turn_within/warn,请注册自定义评估器(hide)和处理程序(register_expectation_evaluator)。register_violation_handler
Default expectations per adapter
各适配器的默认预期配置
| Adapter | Defaults |
|---|---|
| |
| |
| |
| |
| 适配器 | 默认配置 |
|---|---|
| |
| |
| |
| |
Violation handlers
违规处理程序
python
from ag2.network import Expectation
Expectation(name="acks_within", on_violation="auto_close", params={"seconds": 30})default_handlers()on_violation | Handler class | Effect |
|---|---|---|
| | No-op handler. The actual audit record ( |
| | Post |
| | Close the channel with |
There is no built-in or handler — those names appear only on the / manifests and are no-ops until you register a handler for them.
"warn""hide"discussionworkflowpython
from ag2.network import Expectation
Expectation(name="acks_within", on_violation="auto_close", params={"seconds": 30})default_handlers()on_violation | 处理程序类 | 效果 |
|---|---|---|
| | 空操作处理程序。实际的审计记录( |
| | 向所有通道参与者发送 |
| | 关闭通道, |
没有内置的或处理程序——这些名称仅出现在/适配器的清单中,在注册对应的处理程序前均为无效操作。
"warn""hide"discussionworkflowOverriding adapter defaults
覆盖适配器默认配置
Pass in the channel knobs to replace the adapter's defaults:
expectationspython
channel = await alice.open(
type="conversation",
target=bob.agent_id,
knobs={
"expectations": [
{"name": "max_silence", "on_violation": "auto_close",
"params": {"seconds": 600}},
],
},
)在通道knobs中传入以替换适配器的默认配置:
expectationspython
channel = await alice.open(
type="conversation",
target=bob.agent_id,
knobs={
"expectations": [
{"name": "max_silence", "on_violation": "auto_close",
"params": {"seconds": 600}},
],
},
)Custom evaluators
自定义评估器
python
from ag2.network import EV_TEXT, Expectation
from ag2.network import ExpectationContext, Violation
class TooManyMessagesEvaluator:
name = "too_many_messages"
# Signature: evaluate(self, expectation, context) -> Violation | None
def evaluate(self, expectation: Expectation, context: ExpectationContext) -> Violation | None:
threshold = int(expectation.params["max"])
text_count = sum(1 for e in context.wal if e.event_type == EV_TEXT)
if text_count > threshold:
return Violation(
expectation=expectation, # the Expectation object, not a string
violator_ids=[], # channel-wide
detail={"text_count": text_count, "threshold": threshold},
)
return NoneViolationViolation(expectation: Expectation, violator_ids: list[str] = [], detail: dict = {})expectationExpectationnameon_violationparamschannel_idhub.register_expectation_evaluator(TooManyMessagesEvaluator())python
from ag2.network import EV_TEXT, Expectation
from ag2.network import ExpectationContext, Violation
class TooManyMessagesEvaluator:
name = "too_many_messages"
# 签名:evaluate(self, expectation, context) -> Violation | None
def evaluate(self, expectation: Expectation, context: ExpectationContext) -> Violation | None:
threshold = int(expectation.params["max"])
text_count = sum(1 for e in context.wal if e.event_type == EV_TEXT)
if text_count > threshold:
return Violation(
expectation=expectation, # Expectation对象,而非字符串
violator_ids=[], # 全通道范围
detail={"text_count": text_count, "threshold": threshold},
)
return NoneViolationViolation(expectation: Expectation, violator_ids: list[str] = [], detail: dict = {})expectationExpectationnameon_violationparamschannel_idhub.register_expectation_evaluator(TooManyMessagesEvaluator())Deterministic testing
确定性测试
python
hub = await Hub.open(MemoryKnowledgeStore(), expectation_sweep_interval=0)python
hub = await Hub.open(MemoryKnowledgeStore(), expectation_sweep_interval=0)Manually advance state and tick:
手动推进状态并触发评估:
clock.advance(45)
await hub._expectation_tick() # operator API (leading underscore by convention)
undefinedclock.advance(45)
await hub._expectation_tick() # 操作符API(按约定以开头下划线标识)
undefinedAudit log
审计日志
The hub maintains an append-only audit log ( instance), exposed via the public property (the internal attribute is ; swap the instance with ):
AuditLoghub.audit_loghub._audit_loghub.replace_audit_log(...)python
records = await hub.audit_log.read_all()
for r in records:
print(r["kind"], r["at"], r)Each record is a plain dict with at minimum and (ISO-Z timestamp); kind-specific fields appear alongside.
kindat中心节点维护一个追加式审计日志(实例),通过公共属性暴露(内部属性为;可通过替换实例):
AuditLoghub.audit_loghub._audit_loghub.replace_audit_log(...)python
records = await hub.audit_log.read_all()
for r in records:
print(r["kind"], r["at"], r)每条记录是一个普通字典,至少包含和(ISO-Z时间戳);特定类型的字段会附加在其中。
kindatAudit kinds
审计类型
python
from ag2.network import (
AUDIT_KIND_AGENT_REGISTERED,
AUDIT_KIND_AGENT_UNREGISTERED,
AUDIT_KIND_RESUME_SET,
AUDIT_KIND_SKILL_SET,
AUDIT_KIND_RULE_SET,
AUDIT_KIND_CHANNEL_CREATED,
AUDIT_KIND_CHANNEL_CLOSED,
AUDIT_KIND_CHANNEL_EXPIRED,
AUDIT_KIND_TASK_TERMINATED,
AUDIT_KIND_EXPECTATION_VIOLATED,
)| Kind | When | Common fields |
|---|---|---|
| | |
| | |
| | Source: |
| | Updated skill markdown |
| | The new |
| | |
| Any close route | |
| TTL sweeper | TTL details |
| | |
| Expectation evaluator's threshold elapsed | |
python
from ag2.network import (
AUDIT_KIND_AGENT_REGISTERED,
AUDIT_KIND_AGENT_UNREGISTERED,
AUDIT_KIND_RESUME_SET,
AUDIT_KIND_SKILL_SET,
AUDIT_KIND_RULE_SET,
AUDIT_KIND_CHANNEL_CREATED,
AUDIT_KIND_CHANNEL_CLOSED,
AUDIT_KIND_CHANNEL_EXPIRED,
AUDIT_KIND_TASK_TERMINATED,
AUDIT_KIND_EXPECTATION_VIOLATED,
)| 类型 | 触发时机 | 常见字段 |
|---|---|---|
| | |
| | |
| | 来源: |
| | 更新后的技能markdown |
| | 新的 |
| | |
| 任何关闭路径 | |
| TTL清理任务 | TTL详情 |
| | |
| 预期评估器的阈值时间到 | |
Common queries
常见查询
python
undefinedpython
undefinedAll violations on the system.
系统中所有违规记录。
violations = [r for r in await hub.audit_log.read_all()
if r["kind"] == AUDIT_KIND_EXPECTATION_VIOLATED]
violations = [r for r in await hub.audit_log.read_all()
if r["kind"] == AUDIT_KIND_EXPECTATION_VIOLATED]
Everything that happened on one channel.
某一通道的所有事件。
channel_records = [r for r in await hub.audit_log.read_all()
if r.get("channel_id") == channel_id]
channel_records = [r for r in await hub.audit_log.read_all()
if r.get("channel_id") == channel_id]
All registrations for one tenant.
某一租户的所有注册记录。
acme_agents = [r for r in await hub.audit_log.read_all()
if r["kind"] == AUDIT_KIND_AGENT_REGISTERED
and r.get("owner") == "acme"]
The audit log is **durable when backed by `DiskKnowledgeStore`**; with `MemoryKnowledgeStore` it lives only as long as the hub.acme_agents = [r for r in await hub.audit_log.read_all()
if r["kind"] == AUDIT_KIND_AGENT_REGISTERED
and r.get("owner") == "acme"]
当使用`DiskKnowledgeStore`作为后端时,审计日志是**持久化**的;使用`MemoryKnowledgeStore`时,审计日志仅在中心节点运行期间存在。Hub listeners — live programmatic observability
中心节点监听器——实时可编程可观测性
The audit log is the durable record. For live reactions to hub state changes — push to a metrics backend, stream to a dashboard, alert an on-call — register a : a read-only Protocol the hub fans out to after every state transition has committed. (The built-in audit log is itself one of these listeners.)
HubListener| Method (exact signature) | Fires when |
|---|---|
| an envelope was accepted, WAL-appended, folded, and dispatched |
| the arbiter / validation denied a send ( |
| delivery to one recipient raised ( |
| |
| |
| an expectation evaluator emitted a |
| an agent's notify-handler turn raised (the default handler routes failures here) |
| a |
| a recipient's inbox first crosses |
All methods are ; the hub awaits them sequentially in registration order, each wrapped in — a buggy listener can't stall dispatch. Keep them fast (queue I/O onto your own task). Subclass (every method is a ) and override only what you need:
asynctry/exceptBaseHubListenerpasspython
from ag2.network import BaseHubListener
class MetricsListener(BaseHubListener):
async def on_envelope_posted(self, envelope, metadata):
statsd.incr(f"net.envelope.{envelope.event_type}")
async def on_inbox_pressure(self, agent_id, pending, cap):
statsd.gauge(f"net.inbox.{agent_id}", pending / cap)
async def on_turn_failed(self, channel_id, agent_id, envelope_id, exc):
sentry.capture_exception(exc)
hub.register_listener(MetricsListener()) # hub.unregister_listener(inst) to detachTwo related hub-subclass seams:
- hooks on
on_*itself — the same method set exists as empty methods onHub; aHubsubclass can override them directly (the fan-out invokes the bound method alongside registered listeners). Use a subclass when the observability is the hub variant you're shipping; useHubfor pluggable add-ons.register_listener - /
hub.register_sweeper(name, interval_seconds, fn)— adds your own periodic coroutine to the hub's interval-sweeper machinery (alongside the built-in TTL and expectation sweepers). Subclass-registered sweepers start immediately ifunregister_sweeper(name)has already run, otherwise queue until it does.Hub.start()
on_inbox_pressurelimits.inbox.high_waterInboxBlockint | NoneNoneint(limits.inbox.max_pending * 0.8)0InboxFulllimits.inbox.max_pendingauthorize_inbox审计日志是持久化记录。对于中心节点状态变更的实时响应——推送至指标后端、流式传输至仪表盘、告警值班人员——请注册****:一个只读协议,中心节点在每次状态变更提交后会广播给所有注册的监听器。(内置审计日志本身就是其中一个监听器。)
HubListener| 方法(精确签名) | 触发时机 |
|---|---|
| 信封被接受、写入WAL、折叠并分发后 |
| 仲裁器/验证拒绝发送( |
| 向某一收件人分发消息失败( |
| |
| |
| 预期评估器触发 |
| 智能体的通知处理程序执行失败(默认处理程序将失败路由至此) |
| 观测到 |
| 收件人收件箱首次超过 |
所有方法均为;中心节点按注册顺序依次等待执行,每个方法都包裹在中——有bug的监听器不会阻塞分发。请保持方法执行快速(将I/O任务放入自己的队列)。继承(所有方法默认)并仅覆盖需要的方法:
asynctry/exceptBaseHubListenerpasspython
from ag2.network import BaseHubListener
class MetricsListener(BaseHubListener):
async def on_envelope_posted(self, envelope, metadata):
statsd.incr(f"net.envelope.{envelope.event_type}")
async def on_inbox_pressure(self, agent_id, pending, cap):
statsd.gauge(f"net.inbox.{agent_id}", pending / cap)
async def on_turn_failed(self, channel_id, agent_id, envelope_id, exc):
sentry.capture_exception(exc)
hub.register_listener(MetricsListener()) # 使用hub.unregister_listener(inst)移除两个相关的中心节点子类接口:
- 自身的
Hub钩子——on_*类本身也有一组空方法;Hub子类可直接覆盖这些方法(广播时会同时调用绑定的方法和注册的监听器)。当可观测性是你要发布的中心节点变体时,使用子类;对于可插拔的附加功能,使用Hub。register_listener - /
hub.register_sweeper(name, interval_seconds, fn)——将自定义的周期性协程添加到中心节点的定时清理机制中(与内置的TTL和预期清理任务一起运行)。如果unregister_sweeper(name)已运行,子类注册的清理任务会立即启动;否则会排队等待启动。Hub.start()
on_inbox_pressurelimits.inbox.high_waterInboxBlockint | NoneNoneint(limits.inbox.max_pending * 0.8)0InboxFulllimits.inbox.max_pendingauthorize_inboxTask observation — building the track record
任务观测——构建任务记录
Capability-tagged tasks update an agent's automatically. This is how the network knows that "bob has completed 47 research tasks at a 4.2s median latency."
Resume.observed[capability]带有能力标签的任务会自动更新智能体的。这就是网络如何知道"bob已完成47个研究任务,中位延迟为4.2秒"的方式。
Resume.observed[capability]Tagging a task
为任务添加标签
agent.task(..., capability="X")python
undefinedagent.task(..., capability="X")python
undefined.tool
and .task(...)
live on Agent
, not on the AgentClient
returned
.tool.task(...)AgentAgentClient.tool
和.task(...)
属于Agent
,而非hc.register(...)
返回的AgentClient
。因此请在注册前装饰Agent。
.tool.task(...)Agenthc.register(...)AgentClientby hc.register(...)
. So decorate the Agent before registering.
hc.register(...)—
@worker_agent.tool
async def research(topic: str, ctx: Context) -> str:
async with worker_agent.task(
f"research: {topic}",
capability="research",
context=ctx,
) as task:
await task.progress({"step": "gather"})
# ... do work ...
await task.complete({"items_found": 7})
return "researched"
worker = await worker_hc.register(worker_agent, Passport(name="worker"), Resume())
Pass `context=ctx` so the task fires its events on the LLM-turn's stream — that's the stream the `TaskMirror` is attached to. Without it, the events never reach the hub.
If `capability=None` (the default), lifecycle events still go to the hub's audit log but `Resume.observed` is **not** updated. Track record is opt-in.@worker_agent.tool
async def research(topic: str, ctx: Context) -> str:
async with worker_agent.task(
f"research: {topic}",
capability="research",
context=ctx,
) as task:
await task.progress({"step": "gather"})
# ... 执行任务 ...
await task.complete({"items_found": 7})
return "researched"
worker = await worker_hc.register(worker_agent, Passport(name="worker"), Resume())
传入`context=ctx`,使任务在LLM轮次的流上触发事件——`TaskMirror`会监听该流。如果不传入,事件将无法到达中心节点。
如果`capability=None`(默认),生命周期事件仍会写入中心节点的审计日志,但`Resume.observed`**不会**更新。任务记录是可选的。Reading the track record
读取任务记录
python
resume = await hub.get_resume(bob.agent_id)
stat = resume.observed.get("research")
if stat:
print(f"completed={stat.completed}/{stat.n} "
f"failed={stat.failed} "
f"p50_latency={stat.p50_latency_ms}ms")python
@dataclass(slots=True)
class ObservedStat:
n: int = 0 # total terminal events
completed: int = 0
failed: int = 0
expired: int = 0
p50_latency_ms: int | None = None # rolling median of started_at → completed_atLatency is computed from to the terminal event time, using the hub's clock. With a in tests you can construct deterministic latencies.
task_meta.started_atMockClockpython
resume = await hub.get_resume(bob.agent_id)
stat = resume.observed.get("research")
if stat:
print(f"completed={stat.completed}/{stat.n} "
f"failed={stat.failed} "
f"p50_latency={stat.p50_latency_ms}ms")python
@dataclass(slots=True)
class ObservedStat:
n: int = 0 # 终端事件总数
completed: int = 0
failed: int = 0
expired: int = 0
p50_latency_ms: int | None = None # 从started_at到completed_at的滚动中位值延迟由中心节点时钟计算,从到终端事件时间。测试时使用可构造确定性的延迟值。
task_meta.started_atMockClockWhere TaskMirror
fits
TaskMirrorTaskMirror
的作用
TaskMirrorThe default handler auto-attaches a per turn, scoped to the active channel. The mirror subscribes to / / / / events on the LLM-turn's stream, forwards each as an envelope to the hub, and on terminal events with a tag calls to update .
TaskMirrorTaskStartedTaskProgressTaskCompletedTaskFailedTaskExpiredag2.task.*capabilityHub.record_observation(...)Resume.observedYou only attach manually if you've written a custom handler:
TaskMirrorpython
from ag2.network import TaskMirror
from ag2.stream import MemoryStream
mirror = TaskMirror(
hub_client=client._hub_client,
owner_id=client.agent_id,
channel_id=metadata.channel_id,
)
stream = MemoryStream()
sub_ids = mirror.attach(stream)
try:
await client.agent.ask(text, stream=stream)
finally:
mirror.detach(stream, sub_ids)The mirror is attached per turn, not per agent — a new one for each inbound envelope. It also swallows hub-forwarding errors silently; a flaky hub connection should not crash the LLM turn.
默认处理程序会为每个轮次自动附加一个,作用域为当前活跃通道。该镜像会监听LLM轮次流上的////事件,将每个事件转发为信封至中心节点,并在带有标签的终端事件触发时调用以更新。
TaskMirrorTaskStartedTaskProgressTaskCompletedTaskFailedTaskExpiredag2.task.*capabilityHub.record_observation(...)Resume.observed仅当你编写了自定义处理程序时,才需要手动附加:
TaskMirrorpython
from ag2.network import TaskMirror
from ag2.stream import MemoryStream
mirror = TaskMirror(
hub_client=client._hub_client,
owner_id=client.agent_id,
channel_id=metadata.channel_id,
)
stream = MemoryStream()
sub_ids = mirror.attach(stream)
try:
await client.agent.ask(text, stream=stream)
finally:
mirror.detach(stream, sub_ids)镜像是每个轮次附加一次,而非每个智能体附加一次——每个入站信封对应一个新镜像。它还会静默忽略中心节点转发错误;不稳定的中心节点连接不应导致LLM轮次崩溃。
When to skip the capability tag
何时跳过能力标签
Tag only when:
- The task represents a capability you want to track in the agent's resume.
- Failure / latency signals are operationally meaningful (driving routing, alerting, peer ranking).
Untagged tasks still get full lifecycle audit records — just no update. Use untagged tasks for internal book-keeping that doesn't represent an externally-visible capability.
Resume.observed仅在以下情况添加标签:
- 任务代表你想要在智能体简历中跟踪的能力。
- 失败/延迟信号具有运营意义(用于驱动路由、告警、peer排名)。
未标记的任务仍会生成完整的生命周期审计记录——只是不会更新。对于不代表外部可见能力的内部记账任务,使用未标记任务。
Resume.observedCross-cutting pattern: multi-capability worker
跨领域模式:多能力智能体
python
@worker_agent.tool
async def research(topic: str, ctx: Context) -> str:
async with worker_agent.task(f"research: {topic}", capability="research", context=ctx) as t:
# ...
return "..."
@worker_agent.tool
async def summarise(text: str, ctx: Context) -> str:
async with worker_agent.task("summarise", capability="summarisation", context=ctx) as t:
# ...
return "..."After a few channels, holds both and s, each tracked independently. A peer-discovery query ( — see ) can then rank by latency or completion rate.
worker.resume.observed"research""summarisation"ObservedStatpeers(action="find", capability="research")ag2-network-tools-and-viewspython
@worker_agent.tool
async def research(topic: str, ctx: Context) -> str:
async with worker_agent.task(f"research: {topic}", capability="research", context=ctx) as t:
# ...
return "..."
@worker_agent.tool
async def summarise(text: str, ctx: Context) -> str:
async with worker_agent.task("summarise", capability="summarisation", context=ctx) as t:
# ...
return "..."经过几个通道后,会包含和的,各自独立跟踪。peer发现查询(——见)可根据延迟或完成率进行排名。
worker.resume.observed"research""summarisation"ObservedStatpeers(action="find", capability="research")ag2-network-tools-and-viewsReading hub state
读取中心节点状态
| Call | Returns |
|---|---|
| |
| Current |
| Current |
| Registered passports; |
| Ordered list of |
| Every audit record |
| The active |
The hub stamps on every mutation, so you can detect stale views by comparing timestamps. For push (vs. these pull calls), register a .
Resume.last_updatedHubListener| 调用 | 返回值 |
|---|---|
| |
| 当前 |
| 当前 |
| 已注册的护照; |
| 该通道中有序的 |
| 所有审计记录 |
| 活跃的 |
中心节点会在每次变更时标记,因此你可通过比较时间戳检测视图是否过期。如需推送(而非这些拉取调用),请注册。
Resume.last_updatedHubListenerQuick reference — imports
快速参考——导入
python
from ag2.network import (
# Identity
Passport, Resume, ResumeExample, ObservedStat,
# Rules
Rule, AccessBlock, LimitsBlock, RateBlock, InboxBlock,
ChannelTypeAccess, parse_duration,
# Arbiter (swappable access / routing seam)
HubArbiter, BaseHubArbiter, RuleBasedArbiter, Allow, Deny,
# Listeners (live observability) — hub.register_listener(...)
HubListener, BaseHubListener,
# Auth
AuthAdapter, AuthRegistry, AuthBlock, NoAuth, ApiKeyAuth,
# Expectations
Expectation,
ExpectationEvaluator, ExpectationContext,
AcksWithinEvaluator, ReplyWithinEvaluator, MaxSilenceEvaluator,
AuditHandler, NotifyChannelHandler, AutoCloseHandler,
Violation, ViolationHandler,
default_evaluators, default_handlers,
# Audit kinds
AUDIT_KIND_AGENT_REGISTERED,
AUDIT_KIND_AGENT_UNREGISTERED,
AUDIT_KIND_RESUME_SET,
AUDIT_KIND_SKILL_SET,
AUDIT_KIND_RULE_SET,
AUDIT_KIND_CHANNEL_CREATED,
AUDIT_KIND_CHANNEL_CLOSED,
AUDIT_KIND_CHANNEL_EXPIRED,
AUDIT_KIND_TASK_TERMINATED,
AUDIT_KIND_EXPECTATION_VIOLATED,
RESUME_SOURCE_OBSERVED, RESUME_SOURCE_TENANT,
# Task observation
TaskMirror,
# Errors — the full family (no `LimitsExceeded`; limit/access denials raise AccessDeniedError, inbox-full raises InboxFull)
NetworkError, AccessDeniedError, AuthError, InboxFull, NotFoundError, ProtocolError,
)python
from ag2.network import (
# 身份标识
Passport, Resume, ResumeExample, ObservedStat,
# 规则
Rule, AccessBlock, LimitsBlock, RateBlock, InboxBlock,
ChannelTypeAccess, parse_duration,
# 仲裁器(可替换的访问/路由接口)
HubArbiter, BaseHubArbiter, RuleBasedArbiter, Allow, Deny,
# 监听器(实时可观测性)——hub.register_listener(...)
HubListener, BaseHubListener,
# 身份验证
AuthAdapter, AuthRegistry, AuthBlock, NoAuth, ApiKeyAuth,
# 预期配置
Expectation,
ExpectationEvaluator, ExpectationContext,
AcksWithinEvaluator, ReplyWithinEvaluator, MaxSilenceEvaluator,
AuditHandler, NotifyChannelHandler, AutoCloseHandler,
Violation, ViolationHandler,
default_evaluators, default_handlers,
# 审计类型
AUDIT_KIND_AGENT_REGISTERED,
AUDIT_KIND_AGENT_UNREGISTERED,
AUDIT_KIND_RESUME_SET,
AUDIT_KIND_SKILL_SET,
AUDIT_KIND_RULE_SET,
AUDIT_KIND_CHANNEL_CREATED,
AUDIT_KIND_CHANNEL_CLOSED,
AUDIT_KIND_CHANNEL_EXPIRED,
AUDIT_KIND_TASK_TERMINATED,
AUDIT_KIND_EXPECTATION_VIOLATED,
RESUME_SOURCE_OBSERVED, RESUME_SOURCE_TENANT,
# 任务观测
TaskMirror,
# 错误——完整的错误家族(无`LimitsExceeded`;限制/访问拒绝抛出AccessDeniedError,收件箱满抛出InboxFull)
NetworkError, AccessDeniedError, AuthError, InboxFull, NotFoundError, ProtocolError,
)