
Ag2 Network Tools And Views
- 30 installs
- 8 repo stars
- Updated July 27, 2026
- ag2ai/ag2-skills
ag2-network-tools-and-views is a Claude Code skill that shapes what an AG2 network agent perceives (views) and which actions its LLM can take (tools and handlers).
About
This skill shapes what an AG2 network agent perceives and which actions its LLM can take. It covers the five auto-injected NetworkPlugin tools (delegate, peers, channels, tasks, context) plus the adapter-owned say, replacing the default handler for gateways and headless workers, the ViewPolicy protocol, peer discovery via skill markdown, and the Envelope wire format with its EV_* event taxonomy.
- Shapes what an AG2 network agent perceives and which actions its LLM can take
- Covers the five auto-injected plugin tools plus the adapter-owned say
- Custom envelope handlers for gateways and headless workers, plus view policies
Ag2 Network Tools And Views by the numbers
- 30 all-time installs (skills.sh)
- Ranked #9,316 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
ag2-network-tools-and-views capabilities & compatibility
Free skill; requires an LLM provider API key to run the agents.
- Capabilities
- agent network · tool surface · view policy · custom handler
- Use cases
- orchestration
- Pricing
- Bring your own API key
npx skills add https://github.com/ag2ai/ag2-skills --skill ag2-network-tools-and-viewsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 8 |
| Last updated | July 27, 2026 |
| Repository | ag2ai/ag2-skills ↗ |
What it does
Customise an AG2 network agent's LLM tool surface, write a custom envelope handler, or wire peer discovery.
Who is it for?
Developers customising the LLM's network surface, writing custom handlers, or building gateways / headless workers.
Skip if: Hub-side governance (rules, expectations, audit), which is ag2-network-governance.
When should I use this skill?
The user wants to customise the LLM's network surface, write a custom envelope handler, build a gateway or headless worker, or wire peer discovery.
What you get
The agent's tools, view of the channel, and envelope handling are customised to the deployment.
By the numbers
- 5 auto-injected plugin tools (delegate, peers, channels, tasks, context)
- 3 built-in views (FullTranscript, WindowedSummary, NamedWindowedSummary)
Files
AG2 Network — Tools, Views & Custom Handlers
Everything on the agent/client side of the network — the mirror image of ag2-network-governance (which is hub-side). This covers what the LLM sees of the channel (views), which actions it can take (the five auto-injected plugin tools plus the adapter-owned say), what other agents know about it (skill markdown), how to replace the handler entirely, and the full Envelope reference.
Prerequisite: readag2-network-quickstartfirst. This skill assumes you knowHub.open,HubClient.register, the channel lifecycle, and basicagent_client.open(...)/channel.send(...).
When to use
- "Limit / extend the LLM's network tool surface" (plugin tools vs. adapter
tools_for) - "Write a custom envelope handler"
- "Build a gateway / headless worker that doesn't run an LLM"
- "Add a non-LLM participant — a person at a UI, a bridge, a scripted seeder" (
HumanClient) - "Customise what each agent sees of the channel history (view policy)"
- "Strip / redact / filter envelopes before they reach the LLM"
- "Wire peer discovery via skill markdown"
- "Send a custom event type (
myapp.review_request, …)" - "I need the
EV_*constants list /Envelopeshape /audiencesemantics"
Network tools — plugin tools vs. adapter tools
An agent's per-turn LLM tool list is assembled from two streams:
1. Plugin tools — when you register with the default attach_plugin=True, NetworkPlugin adds five identity-level, channel-agnostic verbs to agent.tools: peers / channels / tasks / context / delegate. Same behaviour in any channel. 2. Adapter tools — the channel's adapter offers channel-specific tools per turn via adapter.tools_for(client, metadata, state, participant_id); the default handler resolves them and merges them into agent.ask(tools=...) (cached per (adapter, client) so the schema build cost is paid once). The only built-in adapter tool is say.
So say is not a plugin tool — it's offered by the adapter, gated by turn state:
| Adapter | Offers say to… |
|---|---|
consulting | the participant whose turn it is in the 1Q1R (initiator before the prompt; respondent after, before the reply) |
conversation | every participant, always |
discussion | only expected_next_speaker |
workflow | nobody — routing is your @tool handoff functions; the adapter returns [] |
That's why attach_plugin=False no longer controls whether the LLM sees say — it controls the five plugin verbs only. (See ag2-network-quickstart → "Plugin tools vs. adapter tools" for the user-facing summary.)
delegate and say
| Tool | Stream | Signature | Purpose |
|---|---|---|---|
delegate | plugin | delegate(target, prompt, capability?, timeout=300.0) | One-shot consult — opens a consulting channel with target, sends prompt, awaits the single reply, returns its text. A separate channel, so it's safe to call mid-turn on any channel. |
say | adapter (consulting / conversation / discussion) | say(content, audience?, channel_id?) | Post an EV_TEXT into the active channel (or a specified one the agent participates in). audience is a list of peer names (resolved to ids); None broadcasts. Envelope shape comes from adapter.build_text_envelope(...) — the same Layer-2 helper a non-AG2 bridge would call. |
# The LLM emits, e.g.:
delegate(target="bob", prompt="What's the right way to model X?", capability="modeling")
say(content="Here's my answer: …")The framework resolves ChannelInject (current channel), ChannelStateInject, and AgentClientInject automatically inside the notify handler — the LLM never sees those parameters.
Four grouped action-dispatch tools
Each takes an action literal plus action-specific args, keeping the LLM's tool list short.
`peers(action)` — discovery
| Action | Args | Returns |
|---|---|---|
"find" | query?, capability?, sort_by?, limit=20 | List of peer summaries (excludes the caller). |
"describe" | name | One peer's full profile: {passport, resume, skill_md}. skill_md falls back to a rendered passport+resume when no SKILL.md is registered. |
`channels(action)` — lifecycle
| Action | Args | Returns |
|---|---|---|
"list" | `state="active"\ | "all"` |
"open" | type, target, knobs?, intent?, ttl?, message? | Mirrors agent_client.open. If message is given, it's sent as the first envelope on the initiator's behalf right after the channel reaches OPENED (handy for consulting/workflow seeding); a failed seed closes the channel with reason seed_failed. Returns {channel_id, type, participants[, seed_envelope_id]}. |
"info" | channel_id | Full ChannelMetadata if the agent participates. |
"close" | channel_id? (defaults to current) | Closes with reason "closed_by_agent". |
`tasks(action)` — task lifecycle
Two halves: active actions (the agent is inside its own agent.task(...) block) and observation actions (any task the hub has seen).
| Action | Half | Args | Returns |
|---|---|---|---|
"progress" | active | payload | Emits TaskProgress. |
"complete" | active | result? | Terminal — emits TaskCompleted. |
"list" | observation | `scope="own"\ | "all", state="active"\ |
"status" | observation | task_id | Refreshed TaskMetadata. |
"wait" | observation | task_id, timeout=300, poll_interval=0.1 | Blocks until terminal. |
"start" is intentionally not a tool — calling it from the LLM would bypass the async with agent.task(...) lifecycle that scopes TaskInject correctly. Owners start tasks in their own code; the LLM uses progress / complete once a task is active, and delegate for one-shot remote work.
`context(action)` — past content
| Action | Args | Returns |
|---|---|---|
"search" | `query, scope="channel"\ | "knowledge", limit=10` |
"quote" | speaker, recent_n=1, channel_id? | The last recent_n EV_TEXT envelopes from speaker. |
scope="knowledge" reaches into the calling agent's own KnowledgeStore (substring search only — for vector / semantic search, the agent's own loop calls framework-core recall directly).
Adapter-owned tools (tools_for) — and the say double-send
The default handler resolves adapter.tools_for(client, metadata, state, participant_id) each turn and merges the result into agent.ask(tools=...) (alongside the agent's own @tools and any plugin tools). Adapters that take no LLM input — workflow — return []. The others return say, gated by turn state (table above). An adapter could return richer tools too; say is just the only built-in.
Implication for workflow: a workflow agent never sees `say` — its only path to "say something" is the round-end EV_PACKET the handler builds from reply.body, plus whatever @tool handoff functions you wrote. The old "approve() then say(...) races the round-end EV_PACKET" failure mode is structurally impossible now.
For consulting / conversation / discussion, say is offered — but you rarely need it. The default handler already posts the round-end envelope (build_round_envelope → EV_TEXT(reply.body), or None if empty), so an agent that just replies with text has already spoken. say is for posting an extra message in a turn, or posting into a different channel the agent participates in (channel_id=).
The hazard: an agent that calls say(content="…") and then also returns a non-empty reply body emits two substantive envelopes in one turn. On conversation that's harmless. On consulting the second one trips the strict 1Q1R adapter — ProtocolError: channel '<id>' is closed. Mitigations, most robust first:
- Don't prompt the agent to call `say`. Its plain reply is the canonical channel reply; the double-send only happens if you explicitly steer it toward
say. - Replace the default handler with one that doesn't also send a round-end envelope — then
sayis the agent's voice (gateway / headless-worker patterns below). attach_plugin=Falsedoes not help here —sayis adapter-owned, not plugin-owned. It only dropsdelegate/peers/channels/tasks/context. Use it for a bare agent, not as asaysuppressor.
Non-LLM participants — HumanClient
HumanClient is the framework's first-class "participant that isn't an Agent": no LLM, no NetworkPlugin, no assembly policies. A person at a UI, a bridge to another system, a scripted "user" that seeds a workflow — all of these are a HumanClient. Register with register_human (not register, which now rejects kind="human" and points you here):
from autogen.beta.network import HumanClient, Passport
user = await hc.register_human(Passport(name="user", kind="human")) # resume=, rule=, auto_ack_invites= optionalIt satisfies the NetworkClient Protocol — same outbound surface as AgentClient:
ch = await user.open(type="consulting", target="analyst") # initiate a channel
await user.send(ch.channel_id, "What's our Q3 exposure?") # EV_TEXT convenience
await user.post_envelope(env) # escape hatch — adapter-shaped envelopes (e.g. workflow EV_PACKET via adapter.build_packet_envelope)…plus two ways to consume inbound envelopes (an AgentClient only has the notify-handler callback; HumanClient adds an explicit queue):
| Surface | Call | Semantics |
|---|---|---|
| Push | user.on_envelope(callback) | async callback fires once per inbound envelope; multiple callbacks compose in registration order; a raising callback is logged, never propagated. remove_envelope_callback(cb) to detach. |
| Pull | await user.next_envelope(*, predicate=None, timeout=None) | Blocks until the next envelope matching predicate (or any, if None); raises on timeout. |
| Pull (stream) | async for env in user.envelopes(): ... | Yields every inbound envelope until user.disconnect(). |
auto_ack_invites=True (default) makes the human auto-accept channel invites so the hub's quorum handshake completes without a UI round-trip; pass auto_ack_invites=False to gate joins by hand (you'll then post_envelope an EV_CHANNEL_INVITE_ACK yourself). hub.list_agents(kind="human") discovers humans; hub.list_agents(kind="agent") / kind="remote_agent" filter the others.
Typical roles: the kickoff seeder for a workflow channel (FromSpeaker(user) → AgentTarget(first_agent) — see ag2-network-workflow), the human leg of a consulting Q&A, a participant in a discussion round-robin, or a WebSocket/CLI bridge in front of any of those. For a headless agent (an AgentClient that shouldn't run an LLM but still wants the plugin tools / tools_for resolution) you replace its notify handler instead — next.
Replacing the default handler
The default handler does all the "agent receives envelope → auto-ack invites → run LLM (with adapter.tools_for(...) merged in) → post round-end envelope" wiring. Replace it for headless workers, gateways, or any agent that shouldn't run an LLM. (For a participant that was never meant to have an LLM at all, reach for HumanClient above instead of an AgentClient with a custom handler.)
Opting out of the plugin (and/or the default handler)
worker = await hc.register(agent, passport, resume, attach_plugin=False) # no peers/channels/tasks/context/delegate
worker.on_envelope(my_custom_handler) # ← this is what swaps the handlerThese are two independent knobs:
attach_plugin=FalseskipsNetworkPlugin.register(agent)— the agent's tool list losespeers/channels/tasks/context/delegate(and theNetworkContextPolicyprefix). It does not touch the notify handler, and it does not removesay(that's adapter-owned — see above).- The default notify handler is active regardless (it's wired by
AgentClient, not the plugin — unless you registered withattach_default_handler=False). Callclient.on_envelope(callback)to replace it; callclient.on_envelope(client._run_default_handler)to restore it.
What you lose when you call `client.on_envelope(callback)`:
| The default handler does… | If you don't replicate it… |
|---|---|
Auto-ack EV_CHANNEL_INVITE (post EV_CHANNEL_INVITE_ACK) | the channel sits in PENDING until invite_ack_timeout (30s) and the hub closes it (reason invite_timeout) on you |
Run _process_substantive on EV_TEXT / EV_PACKET: read WAL, project the view, stamp dependencies, attach TaskMirror, resolve adapter.tools_for(...), call agent.ask(...), post the round-end envelope built by adapter.build_round_envelope | the LLM never runs, no reply ever goes back, the channel stalls until an expectation fires |
No-op on ag2.channel.* / ag2.task.* lifecycle envelopes | (harmless to skip, but easy to be surprised when these arrive) |
The cheap way to keep most of that for free: handle the events you care about yourself, delegate the rest to `default_handler`.
Handler signature. A callback passed toclient.on_envelope(...)is called with just(envelope,)— the framework doesawait self._on_envelope(envelope). Anyclientreference inside the handler is captured from the enclosing scope (a closure variable). The exporteddefault_handler, by contrast, has signature(envelope, client)— pass both when delegating.
A gateway handler
from autogen.beta.network import Envelope, EV_TEXT, default_handler
async def gateway_handler(envelope: Envelope) -> None:
"""Forward inbound text to an external system instead of running an LLM."""
if envelope.event_type != EV_TEXT:
await default_handler(envelope, client) # auto-ack invites etc. (client from closure)
return
text = envelope.event_data.get("text", "")
await my_external_queue.put({
"from": envelope.sender_id,
"text": text,
"channel": envelope.channel_id,
})
client.on_envelope(gateway_handler)Selective override (fall back to default)
from autogen.beta.network import default_handler, EV_CHANNEL_INVITE
async def selective_handler(envelope: Envelope) -> None:
if envelope.event_type == EV_CHANNEL_INVITE and envelope.sender_id not in TRUSTED_AGENTS:
return # untrusted invite → silent drop; hub will time out and close
await default_handler(envelope, client) # default for everything else
client.on_envelope(selective_handler)Filtered forwarding (pre/post hooks)
async def logged_handler(envelope: Envelope) -> None:
log.info("inbound %s from %s", envelope.event_type, envelope.sender_id)
try:
await default_handler(envelope, client)
finally:
log.info("processed %s", envelope.envelope_id)Bypassing adapter tools
Adapters offer per-turn tools via tools_for(...) (notably say on consulting / conversation / discussion); the default handler merges them into agent.ask(tools=...). If a capable model insists on calling say unprompted (Claude often will) and the resulting double-send is breaking your channel, swap in a custom handler that runs the same substantive path but omits `tools=` from `agent.ask` — the agent's static agent.tools (your @tools + any plugin tools) remain, since ask(tools=…) is additional tools, not a replacement. The shape:
async def no_say_handler(envelope, client):
if envelope.event_type != EV_TEXT:
await default_handler(envelope, client) # invite-ack + lifecycle for free
return
if not await client._hub_client.can_send(envelope.channel_id, client.agent_id):
return # not our turn / channel closing
# ... read_wal_until / resolve_view_policy / stamp_dependencies (hooks above) ...
reply = await client.agent.ask(current_input, stream=stream, dependencies=deps) # ← no tools=
out = adapter.build_round_envelope(metadata=meta, sender_id=client.agent_id, reply=reply,
events=events, state=state, hub=client._hub)
if out is not None:
out.causation_id = envelope.envelope_id
await client.send_envelope(out)That's the default handler's substantive path minus one line. Same trick works to add tools (pass your own tools=[...]) or to swap the view, the dependencies, the round-envelope shape, etc.
Hooks for selective override
If you want to partially replace the default handler's logic, the handler is decomposed into public hooks:
from autogen.beta.network import (
read_wal_until,
resolve_view_policy,
stamp_dependencies,
)| Hook | Purpose |
|---|---|
read_wal_until(client, envelope) | Slice the WAL up to (excluding) the given envelope. |
resolve_view_policy(client, metadata) | The ViewPolicy this participant should use. |
stamp_dependencies(client, channel, state) | Build the context.dependencies dict for the LLM turn (CHANNEL_DEP, AGENT_CLIENT_DEP, HUB_DEP, CHANNEL_STATE_DEP). state is the adapter's current State object (WorkflowState / DiscussionState / …), folded once per turn. |
Use these when your custom handler wants the standard pre-LLM wiring but a custom post-LLM behaviour (or vice versa).
Views — what each LLM sees
ViewPolicy is the projection layer between the channel's WAL and the LLM's history:
class ViewPolicy(Protocol):
name: str
async def project(
self,
wal: list[Envelope],
*,
participant_id: str,
channel: ChannelMetadata,
render_envelope: EnvelopeRenderer, # required — supplied by the adapter
name_for: NameResolver = default_name_resolver, # optional — resolves agent_id → Passport.name
) -> list[BaseEvent]: ...It takes the WAL slice this participant should see and returns a list of BaseEvents that the framework feeds into the LLM turn as pre-populated stream history. render_envelope (an EnvelopeRenderer = Callable[[Envelope], str | None]) is supplied per-call by the channel's adapter so views stay adapter-neutral; name_for (a NameResolver = Callable[[str], str]) labels projection lines and defaults to default_name_resolver (identity). Adapters declare a default; you can override per-channel.
Built-in views
| View | Behaviour | Default for |
|---|---|---|
FullTranscript() | Every visible substantive envelope the adapter's render_envelope returns a string for, in order. Speakers disambiguated by the assistant/user role bit (2-party). | consulting |
WindowedSummary(recent_n=N) | The last N visible substantive envelopes. If there are more, prepends a CompactionSummary placeholder with the count of elided turns. | conversation (recent_n=10) |
NamedWindowedSummary(recent_n=N) | Like WindowedSummary but prefixes each non-self line with [<sender name>]: (resolved via name_for) — needed in N-party channels where the role bit can't tell the "others" apart. | discussion, workflow (recent_n = max(N*2, 4)) |
NamedTranscript() | Like FullTranscript but with the same [<sender name>]: prefix on non-self lines. | (not a default; opt-in for N-party full history) |
All honour audience — an envelope addressed only to [bob] doesn't appear in carol's projection.
from autogen.beta.network import FullTranscript, WindowedSummary, NamedWindowedSummary
view = WindowedSummary(recent_n=12)
projected = await view.project(
wal_slice,
participant_id=carol.agent_id,
channel=metadata,
render_envelope=adapter.render_envelope, # required
name_for=hub.name_for, # optional; defaults to identity
)Resolving the default
from autogen.beta.network import resolve_view_policy
policy = resolve_view_policy(client, metadata)Reads the adapter manifest's default_view_policy and instantiates the matching view from the registry. The default handler calls this once per turn — custom handlers should too unless they're deliberately bypassing the standard projection model.
Custom views
Implement the protocol, give it a unique name, and use it:
from autogen.beta.events import BaseEvent, ModelMessage, ModelRequest, TextInput
from autogen.beta.network import EV_TEXT, ViewPolicy
class FromOneOnly(ViewPolicy):
"""Show only envelopes from a single named sender."""
name = "from_one_only"
def __init__(self, sender_id: str) -> None:
self.sender_id = sender_id
async def project(
self,
wal,
*,
participant_id,
channel,
render_envelope, # required by the protocol
name_for=lambda agent_id: agent_id,
):
out: list[BaseEvent] = []
for env in wal:
if env.event_type != EV_TEXT or env.sender_id != self.sender_id:
continue
text = render_envelope(env) # adapter renders the envelope to a string (or None)
if text is None:
continue
out.append(
ModelMessage(text) if env.sender_id == participant_id
else ModelRequest([TextInput(text)])
)
return outThe BaseEvent types you emit shape how the LLM sees the history: ModelRequest([TextInput(...)]) for "from the user," ModelMessage(...) for "from the assistant," etc. Render the envelope body via the supplied render_envelope callback (so the view stays adapter-neutral) rather than reaching into event_data directly. See autogen.beta.events for the full taxonomy.
Picking a view
- Short, focused exchanges →
FullTranscript(). Token budget isn't the bottleneck; coherence is. - Long-running 2-party exchanges →
WindowedSummary(recent_n=N)withNtuned to turn density. - Long-running N-party discussions / workflows →
NamedWindowedSummary(recent_n=N)so speaker labels survive the window (this is already thediscussion/workflowdefault). - Specialist agents that should ignore unrelated chatter → custom view that filters by audience or tags.
- Privacy-sensitive workflows → custom view that strips fields or redacts before projection.
Switching the view doesn't affect the WAL — every envelope is still there, every operator can still inspect it. Only the LLM's perception of history is shaped.
Skills — how an agent describes itself
Skills are markdown-with-frontmatter that the hub stores verbatim and surfaces to peers during peers(action="describe", name=...) lookup. Pass at registration:
agent_client = await hc.register(
agent,
Passport(name="researcher"),
Resume(claimed_capabilities=["research"]),
skill_md="""\
---
title: Research Assistant
expertise: [policy, finance]
---
# Researcher
A senior policy analyst. Best at:
- Scenario synthesis from multi-source briefs.
- Rebuttal review with confidence scores.
Limitations: not for code review or numerical analysis.
""",
)Parsing the frontmatter
from autogen.beta.network import parse_skill_frontmatter, ParsedSkill
parsed: ParsedSkill = parse_skill_frontmatter(skill_md)
print(parsed.frontmatter) # {"title": "Research Assistant", "expertise": [...]}
print(parsed.body) # the markdown bodyFallback skills
When no skill_md is provided, the hub generates one from the resume so peer lookup doesn't return empty handles:
from autogen.beta.network import render_fallback_skill
skill_md = render_fallback_skill(passport, resume)Useful when constructing skills programmatically — e.g. a tenant uploads a resume but no markdown.
Updating after registration
await hub.set_skill(agent_id, new_skill_md)Emits AUDIT_KIND_SKILL_SET. Same audit shape as set_resume; tenant code can replace skills at any time.
The Envelope wire format
@dataclass(slots=True)
class Envelope:
channel_id: str
sender_id: str # agent_id
audience: list[str] | None # None = broadcast to all participants
event_type: str # "ag2.msg.text", "ag2.channel.invite", etc.
event_data: dict[str, Any] # event-specific payload
envelope_id: str = "" # hub-stamped on accept
task_id: str | None = None
causation_id: str | None = None # envelope_id this one is "in reply to"
trace_id: str | None = None
priority: Priority = "normal" # Priority is a Literal, not an enum (see below)
depth: int = 0 # delegation hop count; hub auto-increments on reply path
idempotency_key: str | None = None # reserved for cross-process transports
created_at: str = "" # hub-stamped ISO-Z on accept
ttl_seconds: int | None = None # per-envelope TTL; None defers to channel expires_atThe hub stamps envelope_id and created_at at admission time (and auto-increments depth on the reply path). Everything else comes from the sender. (There is no sequence field — WAL order is admission order; see "Reading the WAL".)
Substantive events
| Constant | String | event_data |
|---|---|---|
EV_TEXT | "ag2.msg.text" | {"text": "<body>"} |
EV_PACKET | "ag2.packet" | {"routing": {...}, "context_updates": {...}, "body": "<text>"} |
EV_TEXT carries plain text. EV_PACKET is the workflow adapter's atomic round-end capture — routing decision (matched against ToolCalled rules), accumulated context_vars mutations, and final body text bundled into one envelope. Posted by the framework after each Agent.ask round on a workflow channel; tool authors don't construct these directly.
Channel lifecycle events
| Constant | When |
|---|---|
EV_CHANNEL_INVITE | Hub posts to each target when a channel is created. |
EV_CHANNEL_INVITE_ACK | Each invitee posts when accepting. |
EV_CHANNEL_INVITE_REJECT | Optional — invitee rejects (default handler doesn't, but you can override). |
EV_CHANNEL_OPENED | Hub posts when all acks land. |
EV_CHANNEL_CLOSED | Hub posts on any termination path; event_data.reason carries why. |
EV_CHANNEL_EXPIRED | Hub posts when TTL sweeper closes the channel. |
EV_EXPECTATION_VIOLATED | Hub posts when an evaluator's threshold is breached and the handler is notify. |
Context and task events
| Constant | String | When |
|---|---|---|
EV_CONTEXT_SET | "ag2.context.set" | Tool/participant emits to mutate channel-scoped context_vars. event_data is {"set": {...}, "delete": [...]}. |
EV_TASK_CANCEL_REQUEST | "ag2.task.cancel_request" | A peer asks the task owner to cancel. event_data is {"task_id": str, "reason": str}. The owner may honour it (Task.cancel) or ignore it. |
EV_TASK_CANCELLED | "ag2.task.cancelled" | Owner-emitted terminal envelope a peer handler can match on. event_data is {"task_id": str, "reason": str}. |
Task lifecycle (started / progress / completed / failed / expired) is not carried as wire envelopes — it's mirrored from the owner's stream straight into the hub'sTaskMetadatacache byTaskMirror. The twoEV_TASK_*constants above are the exception: cancellation has a peer-driven side that needs an addressable envelope. The default handler no-ops on anyag2.task.*event_type.
Audience and visibility
audience: list[str] | None controls who sees the envelope:
None— broadcast to all participants.[agent_id_1, agent_id_2, ...]— only those participants see it.
from autogen.beta.network import visible_to
if visible_to(env, my_agent_id):
process(env)Views (FullTranscript, WindowedSummary) honour audience — an envelope addressed only to [bob] doesn't appear in carol's projection.
Priority
Priority = Literal["background", "normal", "urgent"]Priority is a string literal type, not an enum — set priority="urgent" (etc.) on the envelope directly. Higher-priority envelopes process ahead of lower in queue order. Use sparingly; most application envelopes leave priority at the default "normal".
Causation
causation_id marks an envelope as "in reply to" another:
await channel.send(reply_text, causation_id=incoming_envelope.envelope_id)The default handler does this automatically when replying to an inbound EV_TEXT. Custom handlers should set it for logical replies — useful for threaded views.
Sending raw envelopes (custom event types)
channel.send(text, audience=...) wraps EV_TEXT for you. For custom event types build an Envelope and post it directly:
from autogen.beta.network import Envelope
envelope = Envelope(
channel_id=channel.channel_id,
sender_id=alice.agent_id,
audience=[bob.agent_id],
event_type="myapp.review_request",
event_data={"document_id": "doc-123", "kind": "security"},
)
await alice.send_envelope(envelope)The hub doesn't validate event_type against any allowlist — custom types pass through unmodified. Adapters fold only the event types they recognise (substantive ones, plus EV_PACKET under workflow, plus lifecycle ones); custom event types land on the WAL and are delivered to the audience but don't advance turn-taking state.
Custom event type guidelines
1. Use a dotted namespace prefix ("myapp.review_request", not "review") to avoid collision with future ag2.* events. 2. Keep event_data JSON-serialisable (no datetimes, dataclasses, etc.) so it round-trips through the store cleanly. 3. If multiple participants should react, set audience=None. If only one, address it specifically; views filter out non-recipients. 4. Don't rely on adapters to do anything with custom types — they pass through. Your custom handler is responsible for processing them.
Reading the WAL
wal = await hub.read_wal(channel.channel_id)
for i, env in enumerate(wal):
print(f"{i:>3} {env.event_type} from={env.sender_id[:8]}")Envelopes appear in admission order (WAL list order — there is no per-envelope sequence counter). The WAL is the canonical replay surface — Hub.hydrate() re-folds it through each adapter to rebuild in-memory state on restart.
Quick reference — imports
from autogen.beta.network import (
# Default handler + hooks (for selective override)
default_handler,
read_wal_until,
resolve_view_policy,
stamp_dependencies,
# Plugin (auto-attached by attach_plugin=True) — peers/channels/tasks/context/delegate.
# `say` is NOT here; it comes from the channel adapter's tools_for(...).
NetworkPlugin,
# Non-LLM participant
HumanClient, # via HubClient.register_human(Passport(name=..., kind="human"))
PassportKind, # Literal["agent", "human", "remote_agent"] | None
# Views
ViewPolicy,
FullTranscript,
WindowedSummary,
NamedTranscript,
NamedWindowedSummary,
# Skills (peer discovery)
ParsedSkill,
parse_skill_frontmatter,
render_fallback_skill,
# Envelopes + events
Envelope,
Priority,
visible_to,
EV_TEXT,
EV_PACKET,
EV_CHANNEL_INVITE,
EV_CHANNEL_INVITE_ACK,
EV_CHANNEL_INVITE_REJECT,
EV_CHANNEL_OPENED,
EV_CHANNEL_CLOSED,
EV_CHANNEL_EXPIRED,
EV_EXPECTATION_VIOLATED,
EV_CONTEXT_SET,
EV_TASK_CANCEL_REQUEST,
EV_TASK_CANCELLED,
# Dependency injection keys (for tests outside the notify handler)
AGENT_CLIENT_DEP,
CHANNEL_DEP,
CHANNEL_STATE_DEP,
HUB_DEP,
TASK_DEP,
# Injects (for tool signatures)
AgentClientInject,
ChannelInject,
ChannelStateInject,
HubInject,
TaskInject,
)Related skills
FAQ
Which five tools does NetworkPlugin inject?
delegate, peers, channels, tasks, and context.
Is say a plugin tool?
No, say is offered by the channel adapter per turn, gated by turn state, not by the plugin.