ag2-network-tools-and-views について
AG2 Network のクライアント側で、エージェントや非 LLM 参加者がネットワークツールを使い、チャネル内容を参照してエンベロープを扱う方法を説明するスキルです。カスタムハンドラー、ゲートウェイ、人間とのブリッジを作る開発者向けに、プラグインツールとアダプターツール、HumanClient、ビュー方針を解説します。
主な機能
- 015 種類のネットワークプラグインツール
- 02アダプター提供のチャネルツール
- 03HumanClient のエンベロープ送受信
- 04標準エンベロープハンドラーの差し替え
- 05チャネル履歴のカスタムビュー
- 06エンベロープとイベント型のリファレンス
活用シーン
- 01エージェントのネットワークツールを制限・拡張
- 02LLM を使わないゲートウェイやワーカーの構築
- 03人や外部システムをチャネルに接続
- 04LLM に渡す履歴のフィルタリングや墨消し
MCP 対応
このスキルには MCP との関係について記載がありません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ag2ai/ ag2-skills --skill ag2-network-tools-and-viewsインストール後、Claude Code で/ag2-network-tools-and-viewsを入力して読み込まれたか確認します。
詳細
AG2 Network — ツール、ビュー、カスタムハンドラー
ネットワークのエージェント/クライアント側にあるすべての機能です。ハブ側の ag2-network-governance を鏡に映したものです。チャネルについて LLM が何を認識するか(ビュー)、どのアクションを実行できるか(自動挿入される5つのプラグインツールと、アダプターが提供する say)、他のエージェントがチャネルについて何を把握しているか(スキルの Markdown)、ハンドラー全体を置き換える方法、そして Envelope の完全なリファレンスを扱います。
前提条件:まず
ag2-network-quickstartをお読みください。このスキルでは、Hub.open、HubClient.register、チャネルのライフサイクル、基本的なagent_client.open(...)/channel.send(...)を理解していることを前提としています。
使用する場面
- 「LLM が使えるネットワークツールの範囲を制限/拡張する」(プラグインツールとアダプターの
tools_for) - 「カスタムエンベロープハンドラーを書く」
- 「LLM を実行しないゲートウェイ/ヘッドレスワーカーを構築する」
- 「LLM 以外の参加者を追加する — UI を操作する人、ブリッジ、スクリプトで動作するシーダー」(
HumanClient) - 「各エージェントに表示するチャネル履歴をカスタマイズする」(ビューのポリシー)
- 「LLM に届く前にエンベロープを除去/編集/フィルタリングする」
- 「スキルの Markdown を使ってピアの検出を設定する」
- 「カスタムイベントタイプを送信する(
myapp.review_requestなど)」 - 「
EV_*定数の一覧/Envelopeの構造/audienceのセマンティクスが必要」
ネットワークツール — プラグインツールとアダプターツール
エージェントの各ターンで使う LLM ツールの一覧は、2つの経路から組み立てられます。
- プラグインツール — デフォルトの
attach_plugin=Trueで登録すると、NetworkPluginがagent.toolsに、ID レベルでチャネルに依存しない5つの動詞を追加します:peers/channels/tasks/context/delegate。どのチャネルでも同じ動作をします。 - アダプターツール — チャネルのアダプターは、
adapter.tools_for(client, metadata, state, participant_id)を通じて、ターンごとにチャネル固有のツールを提供します。デフォルトハンドラーがこれらを解決し、agent.ask(tools=...)にマージします((adapter, client)ごとにキャッシュされるため、スキーマの構築コストは一度だけ発生します)。組み込みのアダプターツールはsayのみです。
つまり、say はプラグインツールではありません。ターンの状態に応じてアダプターが提供します。
| アダプター | say を提供する相手 |
|---|---|
consulting | 1Q1R で自分のターンを迎えている参加者(プロンプト前は開始者、返信前は応答者) |
conversation | 常にすべての参加者 |
discussion | expected_next_speaker のみ |
workflow | 誰にも提供されません — ルーティングには @tool のハンドオフ関数を使います。アダプターは [] を返します |
そのため、attach_plugin=False で say が LLM に表示されるかどうかを制御することはできなくなりました。この設定が制御するのは、5つのプラグイン動詞のみです。(ユーザー向けの概要は ag2-network-quickstart →「プラグインツールとアダプターツール」を参照してください。)
delegate と say
| ツール | 経路 | シグネチャ | 目的 |
|---|---|---|---|
delegate | プラグイン | delegate(target, prompt, capability?, timeout=300.0) | 1回限りの相談です。target との consulting チャネルを開き、prompt を送信して、単一の返信を待ち、そのテキストを返します。別のチャネルを使うため、どのチャネルでもターンの途中で安全に呼び出せます。 |
say | アダプター(consulting/conversation/discussion) | say(content, audience?, channel_id?) | アクティブなチャネル(またはエージェントが参加している指定のチャネル)に EV_TEXT を投稿します。audience はピアの名前のリスト(ID に解決されます)です。None の場合はブロードキャストします。エンベロープの構造は adapter.build_text_envelope(...) から取得します。これは AG2 を使わないブリッジが呼び出す Layer-2 ヘルパーと同じものです。 |
# 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: …")
フレームワークは、通知ハンドラー内で ChannelInject(現在のチャネル)、ChannelStateInject、AgentClientInject を自動的に解決します。LLM にこれらのパラメーターが表示されることはありません。
4 つのアクションをグループ化したディスパッチツール
各ツールは action のリテラルと、アクション固有の引数を受け取るため、LLM のツールリストを短く保てます。
peers(action) — 検出
| アクション | 引数 | 戻り値 |
|---|---|---|
"find" | query?, capability?, sort_by?, limit=20 | ピアの概要リスト(呼び出し元は含みません)。 |
"describe" | name | 1 つのピアの完全なプロフィール:{passport, resume, skill_md}。SKILL.md が登録されていない場合、skill_md はレンダリングした passport と resume にフォールバックします。 |
channels(action) — ライフサイクル
| アクション | 引数 | 戻り値 |
|---|---|---|
"list" | state="active"|"all" | このエージェントが参加しているチャネル。 |
"open" | type, target, knobs?, intent?, ttl?, message? | agent_client.open と同様に動作します。message が指定されている場合、チャネルが OPENED に達した直後、開始者に代わって最初のエンベロープとして送信されます(consulting/workflow のシードに便利です)。シードに失敗すると、理由 seed_failed でチャネルを閉じます。{channel_id, type, participants[, seed_envelope_id]} を返します。 |
"info" | channel_id | エージェントが参加している場合、完全な ChannelMetadata を返します。 |
"close" | channel_id?(デフォルトは現在のチャネル) | 理由 "closed_by_agent" で閉じます。 |
tasks(action) — タスクのライフサイクル
2 つの区分があります。アクティブアクション(エージェント自身の agent.task(...) ブロック内にいる場合)と、観測アクション(ハブが認識している任意のタスク)です。
| アクション | 区分 | 引数 | 戻り値 |
|---|---|---|---|
"progress" | アクティブ | payload | TaskProgress を送信します。 |
"complete" | アクティブ | result? | 終端アクションです。TaskCompleted を送信します。 |
"list" | 観測 | scope="own"|"all", state="active"|"all", limit=20 | タスクの概要。 |
"status" | 観測 | task_id | 更新された TaskMetadata。 |
"wait" | 観測 | task_id, timeout=300, poll_interval=0.1 | タスクが終端状態になるまでブロックします。 |
"start" は意図的にツールではありません。LLM から呼び出すと、TaskInject を適切にスコープする async with agent.task(...) のライフサイクルを迂回してしまいます。タスクはオーナーが自身のコード内で開始します。LLM はタスクがアクティブになった後に progress/complete を使用し、単発のリモート作業には delegate を使用します。
context(action) — 過去のコンテンツ
| アクション | 引数 | 戻り値 |
|---|---|---|
"search" | query, scope="channel"|"knowledge", limit=10 | テキストに query が含まれる抜粋(大文字と小文字を区別しない部分文字列検索)。 |
"quote" | speaker, recent_n=1, channel_id? | speaker からの直近 recent_n 件の EV_TEXT エンベロープ。 |
scope="knowledge" は、呼び出し元エージェント自身の KnowledgeStore を検索します(部分文字列検索のみです。ベクトル検索/セマンティック検索には、エージェント自身のループから framework-core の recall を直接呼び出してください)。
アダプター所有のツール(tools_for) — そして say の二重送信
デフォルトハンドラーは各ターンで adapter.tools_for(client, metadata, state, participant_id) を解決し、その結果をエージェント独自の @tool やプラグインツールと合わせて agent.ask(tools=...) にマージします。LLM からの入力を受け取らないアダプター(workflow)は [] を返します。それ以外のアダプターは、ターンの状態(上の表を参照)に応じて say を返します。アダプターは、さらに豊富なツールを返すこともできます。組み込みのツールは say だけです。
workflow では、ワークフローエージェントが say を受け取ることはありません。「何かを発言する」手段は、ハンドラーが reply.body から構築するラウンド終了時の EV_PACKET と、作成した @tool のハンドオフ関数だけです。以前の「approve() の後に say(...) を呼び出すと、ラウンド終了時の EV_PACKET と競合する」という失敗は、構造上起こり得なくなりました。
consulting/conversation/discussion では say が提供されますが、使う必要はほとんどありません。デフォルトハンドラーはすでにラウンド終了時のエンベロープを投稿するため(build_round_envelope → EV_TEXT(reply.body)、空の場合は None)、テキストで返信するだけでエージェントは発言済みです。say は、ターン内で 追加の メッセージを投稿する場合や、エージェントが参加している 別の チャネルに投稿する場合(channel_id=)に使います。
注意点:エージェントが say(content="…") を呼び出した うえで、空でない返信本文も返すと、1 ターン内に実質的なエンベロープが 2 つ送信されます。conversation では問題ありません。consulting では、2 つ目のエンベロープによって厳密な 1Q1R アダプターで ProtocolError: channel '<id>' is closed が発生します。まず最も確実な対策から、以下を検討してください。
- エージェントに
sayを呼び出すよう指示しないでください。 通常の返信がチャネルへの正式な返信です。二重送信は、sayを使うよう明示的に促した場合にのみ発生します。 - デフォルトハンドラーを置き換えて、ラウンド終了時のエンベロープも送信しないようにします。そうすれば、
sayがエージェントの発言手段になります(以下のゲートウェイ/ヘッドレスワーカーパターンを参照)。 attach_plugin=Falseにしても、ここでは役に立ちません。sayはプラグインではなくアダプターが所有するためです。これによって除外されるのはdelegate/peers/channels/tasks/contextだけです。sayを抑止するためではなく、ツールを持たないエージェントにするために使用してください。
LLM を使用しない参加者 — HumanClient
HumanClient は、フレームワークにおける「Agent ではない参加者」の第一級の存在です。LLM も、NetworkPlugin も、アセンブリポリシーもありません。UI 上の人、別のシステムへのブリッジ、ワークフローを開始するスクリプト化された「ユーザー」などは、すべて HumanClient です。register(現在は kind="human" を拒否してこちらを案内します)ではなく、register_human で登録します。
from ag2.network import HumanClient, Passport
user = await hc.register_human(Passport(name="user", kind="human")) # resume=, rule=, auto_ack_invites= optional
NetworkClient Protocol を満たしており、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)
…さらに、受信エンベロープを利用する方法が 2 つあります(AgentClient が備えるのは通知ハンドラーのコールバックのみですが、HumanClient には明示的なキューが追加されています)。
| インターフェース | 呼び出し | 動作 |
|---|---|---|
| プッシュ | user.on_envelope(callback) | 受信エンベロープごとに async コールバックが 1 回呼び出されます。複数のコールバックは登録順に連結されます。例外を発生させたコールバックはログに記録されますが、例外は伝播しません。切り離すには remove_envelope_callback(cb) を使います。 |
| プル | await user.next_envelope(*, predicate=None, timeout=None) | predicate に一致する次のエンベロープ(None の場合は任意のエンベロープ)を受信するまでブロックします。timeout に達すると例外を発生させます。 |
| プル(ストリーム) | async for env in user.envelopes(): ... | user.disconnect() まで、受信するすべてのエンベロープを返します。 |
auto_ack_invites=True(デフォルト)にすると、人間がチャネルへの招待を自動承認するため、UI を介さずにハブの定足数ハンドシェイクが完了します。参加を手動で制御するには auto_ack_invites=False を渡してください(その場合は自分で post_envelope を使って EV_CHANNEL_INVITE_ACK を送信します)。hub.list_agents(kind="human") で人間を検出できます。hub.list_agents(kind="agent")/kind="remote_agent" では、それ以外を絞り込めます。
典型的な役割としては、workflow チャネルの開始役(FromSpeaker(user) → AgentTarget(first_agent)。ag2-network-workflow を参照)、consulting の Q&A における人間側の参加者、discussion のラウンドロビンに参加する人、またはこれらの前段に置く WebSocket/CLI ブリッジなどがあります。ヘッドレスエージェント(LLM は実行しないものの、プラグインツールや tools_for による解決を引き続き必要とする AgentClient)の場合は、通知ハンドラーを置き換えます。次の項を参照してください。
デフォルトハンドラーの置き換え
デフォルトハンドラーは、「エージェントがエンベロープを受信 → 招待を自動承認 → LLM を実行(adapter.tools_for(...) をマージ)→ ラウンド終了エンベロープを送信」という一連の処理をすべて担います。ヘッドレスワーカー、ゲートウェイ、または LLM を実行すべきでないエージェントでは、これを置き換えてください。(そもそも LLM を使用する想定のない参加者には、カスタムハンドラーを設定した AgentClient ではなく、上記の HumanClient を使用してください。)
プラグイン(および/またはデフォルトハンドラー)を無効にする
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 handler
これらは独立した2つの設定です。
attach_plugin=Falseにすると、NetworkPlugin.register(agent)がスキップされ、エージェントのツールリストからpeers/channels/tasks/context/delegate(およびNetworkContextPolicyのプレフィックス)がなくなります。通知ハンドラーには影響せず、sayも削除されません(これはアダプターが管理します。上記を参照してください)。- デフォルトの通知ハンドラーは、デフォルトで有効です(プラグインではなく
AgentClientによって設定されます。ただし、attach_default_handler=Falseを指定して登録した場合を除きます)。client.on_envelope(callback)を呼び出すと置き換わります。復元するにはclient.on_envelope(client._run_default_handler)を呼び出してください。
client.on_envelope(callback) を呼び出すと失われるもの:
| デフォルトハンドラーの処理… | 代わりの処理を実装しない場合… |
|---|---|
EV_CHANNEL_INVITE を自動承認する(EV_CHANNEL_INVITE_ACK を投稿する) | チャンネルは invite_ack_timeout(30秒)まで PENDING 状態のままとなり、ハブによって(理由 invite_timeout で)閉じられます |
EV_TEXT/EV_PACKET で _process_substantive を実行する:WAL を読み取り、ビューを投影し、依存関係を設定し、TaskMirror をアタッチし、adapter.tools_for(...) を解決し、agent.ask(...) を呼び出し、adapter.build_round_envelope で構築したラウンド終了エンベロープを投稿する | LLM は実行されず、返信も戻らないため、期待値が発火するまでチャンネルが停滞します |
ag2.channel.*/ag2.task.* のライフサイクルエンベロープでは何もしない | (スキップしても問題ありませんが、これらが届いたときに驚くかもしれません) |
多くの処理を手軽にそのまま活用する方法は、必要なイベントを自分で処理し、それ以外は default_handler に委譲することです。
ハンドラーのシグネチャ。
client.on_envelope(...)に渡すコールバックは(envelope,)のみを引数として呼び出されます。フレームワークはawait self._on_envelope(envelope)を実行します。ハンドラー内のclient参照は、外側のスコープから(クロージャ変数として)取得します。一方、エクスポートされているdefault_handlerのシグネチャは(envelope, client)です。委譲する際は両方を渡してください。
ゲートウェイハンドラー
from ag2.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)
選択的なオーバーライド(デフォルトにフォールバック)
from ag2.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)
フィルター付き転送(前後のフック)
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)
アダプターツールを迂回する
アダプターは各ターンで tools_for(...) を通じてツールを提供します(特に consulting/conversation/discussion の say)。デフォルトハンドラーはそれらを agent.ask(tools=...) にマージします。高性能なモデルが、促されていないのに say を呼び出そうとし(Claude はよくそうします)、その結果の二重送信でチャンネルに問題が生じる場合は、同じ実質処理の流れを実行しつつ、agent.ask から tools= を省略する カスタムハンドラーに切り替えてください。エージェントの静的な agent.tools(自分の @tool とプラグインツール)は引き続き利用できます。ask(tools=…) で指定するのは追加のツールであり、置き換えではありません。構成例:
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)
これは、デフォルトハンドラーの実質処理から1行だけを省いたものです。同じ方法でツールを追加したり(独自の tools=[...] を渡す)、ビュー、依存関係、ラウンドエンベロープの形式などを変更したりすることもできます。
選択的なオーバーライド用フック
デフォルトハンドラーのロジックを部分的に置き換えたい場合、ハンドラーの処理は公開フックに分割されています。
from ag2.network import (
read_wal_until,
resolve_view_policy,
stamp_dependencies,
)
| フック | 目的 |
|---|---|
read_wal_until(client, envelope) | 指定されたエンベロープまで(そのエンベロープは含めずに)WAL を切り出します。 |
resolve_view_policy(client, metadata) | この参加者が使用する ViewPolicy を返します。 |
stamp_dependencies(client, channel, state) | LLM のターン用に context.dependencies 辞書(CHANNEL_DEP、AGENT_CLIENT_DEP、HUB_DEP、CHANNEL_STATE_DEP)を構築します。state はアダプターの現在の State オブジェクト(WorkflowState/DiscussionState/…)で、ターンごとに一度だけ折りたたまれます。 |
カスタムハンドラーで、LLM 呼び出し前は標準の設定処理を使いつつ、呼び出し後の動作は独自にしたい場合(またはその逆の場合)に、これらを使用してください。
ビュー — 各 LLM が見るもの
ViewPolicy は、チャネルの WAL と LLM の履歴との間にある投影レイヤーです。
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]: ...
参加者が見るべき WAL の範囲を受け取り、フレームワークが LLM のターンに事前入力済みのストリーム履歴として渡す BaseEvent のリストを返します。render_envelope(EnvelopeRenderer = Callable[[Envelope], str | None])は、ビューをアダプターに依存しない状態に保つため、チャネルのアダプターから呼び出しごとに渡されます。name_for(NameResolver = Callable[[str], str])は投影行にラベルを付けます。デフォルトは default_name_resolver(恒等関数)です。アダプターでデフォルトを宣言でき、チャネルごとに上書きできます。
組み込みビュー
| ビュー | 動作 | デフォルト |
|---|---|---|
FullTranscript() | アダプターの render_envelope が文字列を返す、表示可能な実質的エンベロープをすべて順番に含めます。話者はアシスタント/ユーザーのロールビットで区別されます(2者間)。 | consulting |
WindowedSummary(recent_n=N) | 表示可能な実質的エンベロープのうち、直近の N 件を含めます。件数がそれを超える場合は、省略されたターン数を記載した CompactionSummary のプレースホルダーを先頭に追加します。 | conversation(recent_n=10) |
NamedWindowedSummary(recent_n=N) | WindowedSummary と同様ですが、自分以外の各行の先頭に [<sender name>]: を付けます(name_for で解決)。ロールビットでは他の参加者を区別できない、N者間チャネルで必要です。 | discussion、workflow(recent_n = max(N*2, 4)) |
NamedTranscript() | FullTranscript と同様ですが、自分以外の各行の先頭に同じ [<sender name>]: を付けます。 | (デフォルトではありません。N者間の全履歴を使う場合に明示的に選択します) |
どのビューも audience を尊重します。たとえば、[bob] のみに宛てられたエンベロープは、carol の投影には表示されません。
from ag2.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
)
デフォルトの解決
from ag2.network import resolve_view_policy
policy = resolve_view_policy(client, metadata)
アダプターのマニフェストから default_view_policy を読み取り、レジストリから該当するビューをインスタンス化します。デフォルトハンドラーはこれをターンごとに一度呼び出します。カスタムハンドラーでも、標準の投影モデルを意図的に迂回する場合を除き、同様に呼び出してください。
カスタムビュー
プロトコルを実装し、一意の name を付けて使用します。
from ag2.events import BaseEvent, ModelMessage, ModelRequest, TextInput
from ag2.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 out
出力する BaseEvent の種類によって、LLM に表示される履歴が決まります。「ユーザーから」の内容には ModelRequest([TextInput(...)])、「アシスタントから」の内容には ModelMessage(...) などを使います。event_data に直接アクセスするのではなく、渡された render_envelope コールバックを使ってエンベロープの本文を描画してください(これにより、ビューをアダプターに依存しない状態に保てます)。分類の全容については ag2.events を参照してください。
ビューの選び方
- 短く、焦点の定まったやり取り →
FullTranscript()。トークン予算がボトルネックではなく、一貫性が重要です。 - 長期にわたる2者間のやり取り → ターン密度に合わせて
Nを調整したWindowedSummary(recent_n=N)。 - 長期にわたるN者間の議論/ワークフロー → 話者ラベルがウィンドウ内に残るよう
NamedWindowedSummary(recent_n=N)(これはすでにdiscussion/workflowのデフォルトです)。 - 無関係な会話を無視すべき専門エージェント → オーディエンスやタグでフィルタリングするカスタムビュー。
- プライバシーに配慮が必要なワークフロー → 投影前にフィールドを削除または秘匿するカスタムビュー。
ビューを切り替えても WAL には影響しません。すべてのエンベロープは引き続き保持され、すべてのオペレーターが引き続き確認できます。形作られるのは、LLM が認識する履歴だけです。
スキル — エージェントの自己紹介
スキルはフロントマター付きの Markdown で、ハブにそのまま保存され、peers(action="describe", name=...) の検索時にピアへ提示されます。登録時に渡します。
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.
""",
)
フロントマターの解析
from ag2.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 body
スキルのフォールバック
skill_md が指定されていない場合、ピア検索で空のハンドルが返されないよう、ハブは履歴書からスキルを生成します。
from ag2.network import render_fallback_skill
skill_md = render_fallback_skill(passport, resume)
スキルをプログラムで構築する場合に便利です。たとえば、テナントが履歴書をアップロードしても Markdown を用意していない場合に役立ちます。
登録後の更新
await hub.set_skill(agent_id, new_skill_md)
AUDIT_KIND_SKILL_SET を出力します。監査データの形式は set_resume と同じです。テナントのコードから、いつでもスキルを置き換えられます。
Envelope ワイヤーフォーマット
@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_at
ハブは受け入れ時に envelope_id と created_at を付与し(返信経路では depth を自動的にインクリメントします)、それ以外はすべて送信元が設定します。(sequence フィールドはありません。WAL の順序は受け入れ順です。「WAL の読み取り」を参照してください。)
内容を伴うイベント
| 定数 | 文字列 | event_data |
|---|---|---|
EV_TEXT | "ag2.msg.text" | {"text": "<body>"} |
EV_PACKET | "ag2.packet" | {"routing": {...}, "context_updates": {...}, "body": "<text>"} |
EV_TEXT はプレーンテキストを格納します。EV_PACKET はワークフローアダプターによる、ラウンド終了時のアトミックなキャプチャです。ルーティング決定(ToolCalled ルールとの照合)、蓄積された context_vars の変更、最終的な本文テキストを1つのエンベロープにまとめます。ワークフローチャネル上の各 Agent.ask ラウンドの後にフレームワークが投稿するものであり、ツール作成者が直接構築するものではありません。
チャネルのライフサイクルイベント
| 定数 | 発生時点 |
|---|---|
EV_CHANNEL_INVITE | チャネル作成時に、ハブが各 target に投稿します。 |
EV_CHANNEL_INVITE_ACK | 招待を受け入れた各参加者が投稿します。 |
EV_CHANNEL_INVITE_REJECT | 任意です。招待された参加者が拒否した場合に投稿します(デフォルトのハンドラーは拒否しませんが、オーバーライドできます)。 |
EV_CHANNEL_OPENED | すべての確認応答が届いたときに、ハブが投稿します。 |
EV_CHANNEL_CLOSED | いずれかの終了経路で、ハブが投稿します。event_data.reason に理由が格納されます。 |
EV_CHANNEL_EXPIRED | TTL スイーパーがチャネルを閉じたときに、ハブが投稿します。 |
EV_EXPECTATION_VIOLATED | 評価器のしきい値を超え、ハンドラーが notify に設定されているときに、ハブが投稿します。 |
コンテキストおよびタスクイベント
| 定数 | 文字列 | 発生時点 |
|---|---|---|
EV_CONTEXT_SET | "ag2.context.set" | ツールまたは参加者が、チャネル単位の context_vars を変更するために送信します。event_data は {"set": {...}, "delete": [...]} です。 |
EV_TASK_CANCEL_REQUEST | "ag2.task.cancel_request" | ピアがタスクの所有者にキャンセルを要求します。event_data は {"task_id": str, "reason": str} です。所有者は要求を受け入れても(Task.cancel)、無視してもかまいません。 |
EV_TASK_CANCELLED | "ag2.task.cancelled" | ピアのハンドラーが照合できる、所有者が送信する終端エンベロープです。event_data は {"task_id": str, "reason": str} です。 |
タスクのライフサイクル(開始/進行状況/完了/失敗/期限切れ)は、ワイヤーエンベロープでは伝達されません。
TaskMirrorによって、所有者のストリームからハブのTaskMetadataキャッシュに直接反映されます。上記の2つのEV_TASK_*定数は例外です。キャンセルにはアドレス指定可能なエンベロープを必要とする、ピア主導の処理があるためです。デフォルトのハンドラーは、ag2.task.*イベントタイプをすべて何もせずに処理します。
受信者と可視性
audience: list[str] | None は、エンベロープを誰が閲覧できるかを制御します。
None— すべての参加者にブロードキャストします。[agent_id_1, agent_id_2, ...]— 指定した参加者だけが閲覧できます。
from ag2.network import visible_to
if visible_to(env, my_agent_id):
process(env)
ビュー(FullTranscript、WindowedSummary)は audience を尊重します。[bob] のみに宛てられたエンベロープは、carol のプロジェクションには表示されません。
優先度
Priority = Literal["background", "normal", "urgent"]
Priority は列挙型ではなく文字列リテラル型です。エンベロープに priority="urgent" などを直接設定します。優先度の高いエンベロープは、キュー内で低いものより先に処理されます。優先度の使用は控えめにしてください。ほとんどのアプリケーションエンベロープでは、priority をデフォルトの "normal" のままにします。
原因
causation_id は、あるエンベロープが別のエンベロープへの「返信」であることを示します。
await channel.send(reply_text, causation_id=incoming_envelope.envelope_id)
デフォルトのハンドラーは、受信した EV_TEXT に返信するときにこれを自動的に設定します。カスタムハンドラーでは、論理的な返信にこの値を設定してください。スレッド表示に役立ちます。
生のエンベロープを送信する(カスタムイベントタイプ)
channel.send(text, audience=...) は EV_TEXT でラップします。カスタムイベントタイプの場合は、Envelope を構築して直接投稿します。
from ag2.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)
ハブは event_type を許可リストと照合して検証しません。カスタムタイプは変更されずにそのまま通過します。アダプターは認識するイベントタイプ(内容を伴うイベント、ワークフローでの EV_PACKET、およびライフサイクルイベント)のみを集約します。カスタムイベントタイプは WAL に記録され、受信者に配信されますが、ターン進行状態には影響しません。
カスタムイベントタイプのガイドライン
- 将来の
ag2.*イベントとの衝突を避けるため、ドット区切りの名前空間プレフィックス("review"ではなく"myapp.review_request")を使用します。 - ストアを経由しても正常に往復できるよう、
event_dataはJSON にシリアライズ可能な形式に保ちます(日付時刻やデータクラスなどは使用しないでください)。 - 複数の参加者が反応する必要がある場合は、
audience=Noneに設定します。1 人だけの場合は、その人を指定します。ビューは宛先以外のイベントを除外します。 - カスタムタイプに対してアダプターが何らかの処理を行うことを前提にしないでください。アダプターはそのまま通過させます。処理はカスタムハンドラーが担います。
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]}")
エンベロープは受け入れ順(WAL リストの順序)で表示されます。エンベロープごとの sequence カウンターはありません。WAL はリプレイの正規の基盤です。Hub.hydrate() は、再起動時に各アダプターを介して WAL を再度適用し、インメモリ状態を再構築します。
クイックリファレンス — インポート
from ag2.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,
)