ツール検索と動的カタログ

Markdown
読み込み方針を生成し、読み込み済みツール用の別ストアなしで変化するカタログを利用する。

ツール検索は、モデルが必要とするときに定義を読み込みます。レジストリは、利用側を再ビルドせずにプロバイダーが提供ツールを変更できる仕組みです。この2つは独立しています。静的ツールでも検索を使え、動的ツールでも全定義を最初からモデルに渡せます。

検索で読み込むツールを選ぶ

コンパイル済みの Records ツールセットが lookupsearchanalyze を定義しているとします。よく使う lookup はすぐに利用できるようにし、残りの2つだけ読み込みを遅らせます。

Agent("assistant", "Find and analyze records.", func() {
    Use(Records, func() {
        Deferred("search", "analyze")
    })
})

検索で読み込むのは searchanalyze だけで、lookup は最初からモデルに提示されます。変わるのは定義の読み込み方であり、権限や実行方法ではありません。この指定は利用側の Use 内に置き、共有の Toolset 定義や Export には置きません。共有プロバイダー、エクスポート、他の利用側は変わりません。

名前は、コンパイル済みツールセットで宣言したローカル名と完全に一致する必要があります。"search" を使い、"records.search" や生成された Go 名は使いません。名前による選択は、ローカルツール、ツールとして公開されたエージェント、スキーマを宣言した外部 MCP ツール、Goa ベースの MCP ツールに対応します。

  • Deferred() はその Use の全ツールを選択します。繰り返し指定しても有効です。
  • 名前付きの宣言を複数書くと選択が結合されます。Deferred("search")Deferred("analyze") を続けて書くと両方が選ばれます。
  • 空の名前や重複した名前は拒否されます。別の宣言との重複も対象です。未知の名前は、コンパイル対象の全ツールが揃った後、コード生成時に拒否されます。
  • 同じ Use 内で Deferred() と名前付きの選択を混在させることはできません。

変化するカタログを利用する

変化するカタログには Registry を使います。FromRegistry ツールセットとレジストリ全体はどちらも実行時にツールを解決するため、Deferred での名前付き選択を拒否します。

var Company = Registry("company", func() {
    URL("https://registry.example")
})
var Records = Toolset(FromRegistry(Company, "records"))

var _ = Service("assistant", func() {
    Agent("reader", "Read records.", func() {
        Use(Records, func() { Deferred() })
    })
    Agent("generalist", "Use the company catalog.", func() {
        Use(Company, func() { Deferred() })
    })
})

reader は必須の名前付きツールセットを1つ解決します。generalist は現在一覧にあるすべてを解決します。Deferred() を外すと同じカタログを即時に提示します。名前付きソースの Version("1.2.3") は現在公開中のバージョンを検証するもので、過去の版を選ぶ指定ではありません。

重複・重なりのあるソース、レジストリ参照内のインラインツール宣言、その参照のエクスポートは拒否されます。定義はプロバイダーが所有し、実行ポリシーがモデルに渡す前にカタログを絞り込みます。

レジストリ参照では、利用側による Tags(...) の上書きや PublishTo(...) も拒否します。タグはプロバイダーが所有し、利用側は実行ポリシーで絞り込みます。

接続と公開

アプリケーション起動時に、分散レジストリの生成済みサービスクライアント(registry/gen/registry.Client)と結果受信用の Pulse クライアントを構築します。実行開始前に接続を登録します。

if err := rt.RegisterRegistry("company", registryClient, pulseClient); err != nil {
    return err
}
if err := genreader.RegisterReaderAgent(ctx, rt, genreader.ReaderAgentConfig{
    Planner: myPlanner,
}); err != nil {
    return err
}
client := genreader.NewClient(rt)

Definition()NewClient(rt) はカタログ引数を取らず、ネットワーク通信もしません。コンパイル済みツールは従来の生成済みヘルパーで登録します。レジストリのツールはランタイムが実行するため、独自の検索コールバックや動的エグゼキューターは不要です。HTTP カタログクライアントは対応する HTTP サーバー用の別トランスポートです。

provider は既存の schema fingerprint と登録ライフサイクルを使って、生成された ToolSchemas() の宣言を公開します。ConsumerContract には検索語、フィールド情報、必須ラベル、確認、ページネーション、サーバー専用データが含まれます。動的なサービスツールと native Agent ツール がこれらの契約に対応し、planner 制御ツールはコンパイル済みのままです。スキーマだけの登録や未対応の実行種別は明示的に拒否されます。

実行時に定義するツールと適用範囲別のカタログ

ここで説明する動的 Agent API には Goa-AI v0.84.0 以降が必要です。

生成されたツール package は Toolset() も公開します。宣言された登録名、説明、tag、スキーマの新しいコピーを返します。Go で実行時に作る宣言には runtime/toolregistry/contract.Compile を使います。*genregistry.ToolSchema を検証し、検証用 JSON codec を持つ独立した tools.ToolSpec を返します。metadata は明示的に指定してください。コンパイラーはフィールド名から context や権限を推測しません。

実行時の宣言や、説明と tag を含む完全な Toolset() の値には contract.Fingerprint(toolset) を使います。登録日時は計算に含まれません。既存の生成 helper SchemaFingerprint(name) は、toolset 単位の任意の注釈を含まない provider 登録を表し続けます。生成されたサービスツールの型付き codec は変わりません。

アプリケーションに応じて参照先を選ぶには runtime.RegistryTools を実装し、WithRegistryTools で設定します。Resolve 内の catalog.RunLabels() は現在の run のラベルのコピーを返します。IncludeToolsetIncludeRegistry で参照先を選び、Allows で保存済み呼び出しが引き続き利用できる参照先を決めます。run policy による制限も適用されます。カタログは planning activity ごとに独立し、同時に動く session が互いのツールを変更することはありません。名前空間と認可はアプリケーションが管理します。

検索を実行するのは誰か

  • OpenAI Responses(直接または Bedrock): モデルはクライアント実行のネイティブ検索を要求します。アダプターは、許可された名前・タイトル・説明を単語ベースの関連度アルゴリズム BM25 で順位付けし、該当する定義を返します。初回リクエストにはクエリだけを受け取る検索ツールが入り、名前や説明の一覧は入りません。遅延カタログはアプリケーション内に保持します。
  • Anthropic Messages(直接または Bedrock): 許可済みカタログに遅延読み込みフラグと Claude のホスト型検索ツールを付けて送信します。プロバイダーが検索と定義展開を行います。Bedrock では NewAnthropic、Messages、InvokeModel を使います。Converse は検索に対応していません。
  • その他のアダプター: 未対応の検索は model.ErrToolSearchUnsupported を返します。全定義の即時読み込みへのフォールバックはありません。

プランナーは input.Agent.AdvertisedToolDefinitions() と現在のメッセージを渡し、モデルまたはモデルクラスを明示します。検索処理はアダプター内で完結し、プランナーには通常のツール呼び出しだけが返ります。OpenAI では正の MaxTokens またはアダプターの MaxCompletionTokens が必要です。複数の検索ラウンドで、その論理呼び出しの出力予算を共有します。

カタログ変更と履歴

OpenAI の検索結果では、選択した各関数を、プロバイダー側の関数名と同じ名前のネイティブ名前空間に入れます。これにより Bedrock は、履歴の再送に必要な呼び出し識別情報を完全な形で返します。この表現はアダプターが管理するため、名前空間用の DSL、アプリケーション側のマッピング、読み込み済みツール用の別状態は不要です。即時読み込みのツールは従来の表現を維持します。名前空間で包まずに動的関数を読み込んだ古い Bedrock 履歴には、名前空間のない呼び出しが含まれ、再送時に Bedrock が拒否する場合があります。新しい会話を始めるか、該当するやり取り全体を意図的に履歴から除いてください。アダプターはプロバイダーが返さなかった項目を補いません。

新しい作業を開始できる各計画アクティビティは宣言済みソースを1回読み、推論中はそのカタログを固定します。次のアクティビティは再読込し、前のターン以降に登録されたプロバイダーも取り込みます。最終回答専用の処理や明示的な終了処理はレジストリを読みません。レジストリ全体が空でも有効ですが、必須ソースの不在、版の不一致、ツールIDの重複、読込失敗、解決中の削除は明示的に失敗します。

受理した呼び出しには、選択した定義、固定のページネーション相手があればその定義、既存の登録トークンだけを保存します。確認、結果のデコード、チェックポイント復元は現在のカタログではなく保存済み契約を使います。サービスツールでは、CallResolvedTool は発行前に元のトークンを検証し、発行前に登録が置換されていれば call_not_admitted を記録します。過負荷時の再試行は同じトークンを保持し、その受理対象が置換された場合は admission_conflict を返します。発行済み呼び出しは元の実行先と結果を保持します。

native Agent 呼び出しは選択した executor、設定、結果契約を保持し、子 workflow として実行されます。レジストリの変更は後続の planning activity に反映され、受理済みの呼び出しは変わりません。

ネイティブ検索記録は既存のメッセージメタデータに保存します。永続化や圧縮でも保持してください。読み込み済みツール用の別データベースは不要です。過去の定義は過去の呼び出しを説明し、新規呼び出しの権限は現在の利用宣言とポリシーで決まります。

Claude の追加・削除履歴には対応モデルが必要です。保持中の名前の定義を変更するケースはこのプロトコルで再現できず、拒否されます。新しい会話を開始するか、意図的に履歴を圧縮して対象定義を除いてください。アダプターが黙って履歴をリセットすることはありません。ネイティブ処理のみを含む Claude の pause の自動継続は未実装です。

実行例とアップグレード

Deferred の選択を変更したら、利用側のエージェントを再生成してください。コード生成は検索語の出現回数を準備し、選択したツールの既存の固定 ID を同じランタイム API に渡します。名前付き選択によって、プロバイダー API、プロバイダーの状態、名前空間が追加されることはありません。

v0.80.0 では、Deferred の型が func() から func(...string) に変わりました。Deferred() の呼び出しは引き続き有効ですが、Deferred 自体を func() 型のコールバックとして渡すコードはコンパイルできなくなります。直接渡している箇所を関数で包んでください。

// 変更前
Use(Records, Deferred)

// 変更後
Use(Records, func() { Deferred() })

Deferredfunc() 型のコールバックに代入している他の箇所も同様に変更してください。ツールセット全体を遅延読み込みする動作は変わりません。既存のコールバックを包むだけで選択を変更しない場合、再生成は不要です。

Goa v3.32.0 でプロバイダーと利用側を再生成してください。起動時の DiscoverRegistryToolsets 引数、動的エグゼキューター設定を RegisterRegistry に置き換えます。レジストリを更新して ResolveToolsetCallResolvedTool を提供し、完全な ToolSchemas() を公開してから動的利用を有効にします。スキーマのみの古い登録は既存の静的連携では使えますが、この動的経路では使えません。

確認テンプレートは Go フィールド名 {{ .Key }} ではなく JSON 名 {{ .key }} を使います。JSON 値には {{ json .value }}、省略可能なプロパティには index を使ってください。

goa-ai quickstart には go run ./cmd/tool-search -provider openai -model YOUR_MODEL_ID があり、-provider anthropic も選べます。対応する API キー環境変数が必要です。この任意コマンドは課金対象のモデルを呼びますが、通常の quickstart は認証情報なしで実行できます。ヘルパーは東京という固定の例を返します。ローカル SDK テストで検索・実行・履歴再生を検証しています。ツール1つの例はトークン削減の実証ではありません。対象モデルと実際のカタログで品質と使用量を測定してください。