内部ツールレジストリ
内部ツールレジストリ (Internal Tool Registry) は、プロセス境界をまたいでツールセットの発見と呼び出しを可能にするクラスタ化されたゲートウェイサービスです。ツールセットを別サービスで提供し、消費側エージェントとは独立してスケールさせたい場面向けに設計されています。
カタログは native Agent ツール宣言 も保存し、既存の worker と不変のアプリケーション設定を指定します。利用側 runtime が子 workflow を開始します。provider lease、health ping、Pulse 経由の呼び出しはサービスツールだけに適用されます。
ここで説明する動的 Agent API には Goa-AI v0.84.0 以降が必要です。
概要
レジストリは catalog と gateway の両方として動作します:
- Catalog: エージェントは利用可能なツールセット、スキーマ、ヘルス状態を発見できます
- Gateway: ツール呼び出しはレジストリから Pulse streams 経由で provider へルーティングされます
これにより、エージェントとツールセット provider が疎結合になり、スケール、デプロイ、ライフサイクル管理を独立して行えます。
Tool Registry と Prompt Registry
両者は責務の異なる別システムです:
- Internal Tool Registry (このページ): プロセス境界をまたぐ toolset と tool call の発見/呼び出し。
- Runtime Prompt Registry (
runtime.PromptRegistry): プロセス内での prompt spec 登録とレンダリング。任意で prompt override store (runtime.WithPromptStore) を使えます。
tool registry は prompt template を保存せず、prompt override も解決しません。prompt rendering は runtime/planner 層に残り、prompt_rendered 観測イベントを発行します。
マルチノードクラスタリング
複数の registry node は、設定で同じ Name を使い、同じ Redis インスタンスへ接続することで、同じ論理 registry に参加できます。
同じ名前の node は自動的に次を行います:
- ツールセット登録を共有: Redis の状態を直接読み取ります
- ヘルスチェック ping を協調: toolset ごとに取得する有効期限付き Redis lease を使います
- provider health state を共有: すべての node でヘルス状態を共有
これにより水平スケールと高可用性を実現できます。クライアントは任意の node に接続でき、同じ registry state を参照できます。
クイックスタート
ライブラリとして使う
registry node をプログラムから作成して実行します。registry.New は Redis 上の catalog と call record、Pulse stream、health scheduler を初期化します。Run は gRPC server を起動し、shutdown まで待機します。この例はローカル開発用のアドレスを使います。deployment に合わせて Redis と gRPC の認証情報を設定してください。
package main
import (
"context"
"log"
"github.com/redis/go-redis/v9"
"goa.design/goa-ai/registry"
)
func main() {
ctx := context.Background()
// Connect to Redis
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer rdb.Close()
// Create the registry
reg, err := registry.New(ctx, registry.Config{
Redis: rdb,
Name: "my-registry", // Nodes with same name form a cluster
})
if err != nil {
log.Fatal(err)
}
// Run the gRPC server (blocks until shutdown)
log.Println("starting registry on :9090")
if err := reg.Run(ctx, ":9090"); err != nil {
log.Fatal(err)
}
}
サンプルバイナリ
registry package には、素早くデプロイするための example binary が含まれます。同じ Redis instance を指し、同じ REGISTRY_NAME を持つ node は自動的に cluster を形成します。追加設定なしで toolset registrations を共有し、health checks を協調します。
# Single node (development)
REDIS_URL=localhost:6379 go run ./registry/cmd/registry
# Multi-node cluster (production)
REGISTRY_NAME=prod REGISTRY_ADDR=:9090 REDIS_URL=redis:6379 ./registry
REGISTRY_NAME=prod REGISTRY_ADDR=:9091 REDIS_URL=redis:6379 ./registry
REGISTRY_NAME=prod REGISTRY_ADDR=:9092 REDIS_URL=redis:6379 ./registry
環境変数
| 変数 | 説明 | デフォルト |
|---|---|---|
REGISTRY_ADDR | gRPC listen address | :9090 |
REGISTRY_NAME | Registry cluster name | registry |
REDIS_URL | Redis connection URL | localhost:6379 |
REDIS_PASSWORD | Redis password | (なし) |
PING_INTERVAL | Health check ping interval | 10s |
MISSED_PING_THRESHOLD | unhealthy とみなすまでの missed ping 数 | 3 |
アーキテクチャ
コンポーネント
| コンポーネント | 説明 |
|---|---|
| Service | discovery と invocation のための gRPC handler |
| Catalog | Redis-backed の tool schema、admission token、provider lease、retirement history |
| Health Tracker | ping/pong による provider liveness の監視 |
| Stream Manager | tool call routing 用 Pulse streams の管理 |
| Call Record Store | 各 call の request identity、provider assignment、deadline、publication state、正規 terminal result を保持 |
ツール呼び出しフロー
CallTool が呼ばれると、registry は次を順番に実行します:
- Identity と schema validation: registry は payload を検証し、run 内で一意な
tool_use_idを 1 つ導出します。完全に同じ retry は、保持済みの同じ record へ接続します。 - Provider wait: 未 publish の call は active toolset に healthy provider が現れるまで、既存の execution deadline を上限として待ちます。
- Atomic publication: 1 回の Redis operation で、選んだ provider が現在も active かつ non-draining であることを確認してから request を正確に 1 回 append します。health check 後に rollout が provider を変更した場合、未 publish の call は同じ deadline 内で replacement を選び直します。
- Immutable execution: publish に成功すると provider assignment が固定されます。外部 effect が始まった可能性があるため、その後 call を移動できません。
- Result delivery:
CallToolは正確な provider token、result-stream identity、execution deadline、retention deadline を返します。executor は provider が terminal result を返すか execution deadline が call を確定するまで、その stream を読みます。
publish 前に execution deadline が切れた場合、registry は call_not_admitted を記録します。これは executor が別の plan を選べることを証明します。publish 済み call の結果が不明な場合は outcome_unknown を返し、replacement へ移せません。
Provider 統合 (サービス側)
registry routing は半分にすぎません。provider は toolset 所有サービスプロセス内で tool execution loop を実行する必要があります。
handler を呼び出す前に、provider は message の execution deadline とは独立した、worker のライフサイクルに紐づく context と既存の claim timeout を使って ClaimToolCall を呼び出します。registry は call が期限切れか、すでに最終結果を持つか、別の配信が実行権限を持つかを判断します。provider はこれらの結果を受けると、handler を呼び出したり execution loop を停止したりせず、message の受信確認を行います。execute の判断を受けた場合に限り、message の元の execution deadline を延長せずに handler へ適用して呼び出します。
service-owned で method-backed な toolset (BindTo(...) で宣言された tool) の場合、code generation は次の provider adapter を出力します:
gen/<service>/toolsets/<toolset>/provider.go
生成された provider は次を行います:
- 受信した tool payload JSON を生成 payload codec でデコード
- 生成 transform を使って Goa method payload を構築
- bound service method を呼び出し
- 生成 result codec を使って、tool result JSON と宣言済み server-data をエンコード
次の例は module example.com/registry-provider、service catalog、その method-backed toolset search を使い、catalog.search として登録します。アプリケーションの 2 つの import path と toolset 名を、自分のプロジェクトで生成された値に置き換えてください。NewProvider、ToolSchemas、SchemaFingerprint は生成 toolset package の関数です。生成 schema はそのまま使用します。登録 callback は、module root に生成される AGENTS_QUICKSTART.md の Service-Side Tool Providers の例に沿っています (クイックスタート)。
service の実装、pulse.New(pulse.Options{Redis: rdb}) で作成した Pulse client、deployment の認証情報を指定して grpc.NewClient で作成した registry への gRPC connection を渡します。この process と toolset に対して安定し、稼働中の replica 間で一意な providerID と、同じ登録の replica が共有する deployment 指定の必須値 admissionRevision を渡してください。Serve が incarnation ID を作成し、callback へ渡します。bound service method は context cancellation に従う必要があります。serveTools を service のライフサイクル内で実行し、両 client を開いたまま終了を待ってください。shutdown 時には新しい処理の受け付けを止め、実行権を取得済みの call、result、受信確認を Options.ShutdownTimeout 内で完了させます。この処理が成功した場合だけ、別の時間枠 Registration.ReleaseTimeout で正確な lease を解放します。完了処理に失敗した場合は lease の期限切れによって実行権が終了します。戻り値の error が context.Canceled にも一致する場合でも、完了処理や lease 解放の error を保持して報告してください。以下では必要な登録 callback をすべて設定しています:
package providers
import (
"context"
"encoding/json"
"time"
gencatalog "example.com/registry-provider/gen/catalog"
gensearch "example.com/registry-provider/gen/catalog/toolsets/search"
"goa.design/goa-ai/features/stream/pulse/clients/pulse"
genregistrygrpc "goa.design/goa-ai/registry/gen/grpc/registry/client"
genregistry "goa.design/goa-ai/registry/gen/registry"
registrywire "goa.design/goa-ai/runtime/toolregistry"
"goa.design/goa-ai/runtime/toolregistry/provider"
"google.golang.org/grpc"
)
// serveTools runs the generated catalog provider until shutdown or a provider error.
// The caller owns the clients, service implementation, and deployment identifiers.
func serveTools(ctx context.Context, pulseClient pulse.Client, conn *grpc.ClientConn,
serviceImpl gencatalog.Service, providerID, admissionRevision string) error {
const toolsetID = "catalog.search"
transport := genregistrygrpc.NewClient(conn, grpc.WaitForReady(true))
registryClient := genregistry.NewClient(
transport.Register(),
transport.RenewProvider(),
transport.ReleaseProvider(),
transport.DrainProvider(),
transport.Unregister(),
transport.Pong(),
transport.RegisterAgentToolset(),
transport.ReplaceAgentToolset(),
transport.ListToolsets(),
transport.GetToolset(),
transport.ResolveToolset(),
transport.CheckAdmission(),
transport.Search(),
transport.CallTool(),
transport.CallResolvedTool(),
transport.RetryTool(),
transport.CompleteToolCall(),
transport.PublishToolOutputDelta(),
transport.ReportToolCallOverload(),
transport.ClaimToolCall(),
)
toolSchemas := gensearch.ToolSchemas()
handler := gensearch.NewProvider(serviceImpl)
return provider.Serve(ctx, pulseClient, toolsetID, handler,
provider.Registration{
AdmissionRevision: admissionRevision,
Register: func(ctx context.Context, toolset, providerID, incarnationID, admissionRevision string) (provider.RegistrationLease, error) {
schemaFingerprint, err := gensearch.SchemaFingerprint(toolset)
if err != nil {
return provider.RegistrationLease{}, err
}
result, err := registryClient.Register(ctx, &genregistry.RegisterPayload{
Name: toolset,
Tools: toolSchemas,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
AdmissionRevision: admissionRevision,
WireProtocolVersion: registrywire.WireProtocolVersion,
SchemaFingerprint: schemaFingerprint,
})
if err != nil {
return provider.RegistrationLease{}, err
}
return provider.RegistrationLease{
RegistrationToken: result.RegistrationToken,
Duration: time.Duration(result.LeaseDurationMs) * time.Millisecond,
}, nil
},
Renew: func(ctx context.Context, toolset, providerID, incarnationID, expectedToken string) (time.Duration, error) {
result, err := registryClient.RenewProvider(ctx, &genregistry.RenewProviderPayload{
Name: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
ExpectedRegistrationToken: expectedToken,
})
if err != nil {
return 0, err
}
return time.Duration(result.LeaseDurationMs) * time.Millisecond, nil
},
Drain: func(ctx context.Context, toolset, providerID, incarnationID, expectedToken string, settlementDuration time.Duration) error {
return registryClient.DrainProvider(ctx, &genregistry.DrainProviderPayload{
Name: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
ExpectedRegistrationToken: expectedToken,
SettlementDurationMs: settlementDuration.Milliseconds(),
})
},
Release: func(ctx context.Context, toolset, providerID, incarnationID, expectedToken string) error {
return registryClient.ReleaseProvider(ctx, &genregistry.ReleaseProviderPayload{
Name: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
ExpectedRegistrationToken: expectedToken,
})
},
Complete: func(ctx context.Context, toolset, providerID, incarnationID, providerToken, requestEventID string, result registrywire.ToolResultMessage) error {
resultJSON, err := json.Marshal(result)
if err != nil {
return err
}
return registryClient.CompleteToolCall(ctx, &genregistry.CompleteToolCallPayload{
Toolset: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
RegistrationToken: result.RegistrationToken,
ToolUseID: result.ToolUseID,
ResultJSON: resultJSON,
RequestEventID: requestEventID,
ProviderRegistrationToken: providerToken,
})
},
PublishOutputDelta: func(ctx context.Context, toolset, providerID, incarnationID, providerToken, callToken, toolUseID, requestEventID, stream, delta string) error {
return registryClient.PublishToolOutputDelta(ctx, &genregistry.PublishToolOutputDeltaPayload{
Toolset: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
ProviderRegistrationToken: providerToken,
CallRegistrationToken: callToken,
ToolUseID: toolUseID,
RequestEventID: requestEventID,
Stream: stream,
Delta: delta,
})
},
ReportOverload: func(ctx context.Context, toolset, providerID, incarnationID, providerToken, callToken, toolUseID, requestEventID string) error {
return registryClient.ReportToolCallOverload(ctx, &genregistry.ProviderToolCallClaimPayload{
Toolset: toolset,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
ProviderRegistrationToken: providerToken,
CallRegistrationToken: callToken,
ToolUseID: toolUseID,
RequestEventID: requestEventID,
})
},
Claim: func(ctx context.Context, claim provider.ClaimRequest) (provider.ClaimDisposition, error) {
result, err := registryClient.ClaimToolCall(ctx, &genregistry.ClaimToolCallPayload{
Toolset: claim.Toolset,
ProviderID: claim.ProviderID,
ProviderIncarnationID: claim.ProviderIncarnationID,
ProviderRegistrationToken: claim.ProviderRegistrationToken,
CallRegistrationToken: claim.CallRegistrationToken,
ToolUseID: claim.ToolUseID,
RequestEventID: claim.RequestEventID,
ClaimOperationID: claim.OperationID,
})
if err != nil {
return "", err
}
return provider.ClaimDisposition(result.Disposition), nil
},
},
provider.Options{
ProviderID: providerID,
Pong: func(ctx context.Context, providerID, incarnationID, pingID string) error {
return registryClient.Pong(ctx, &genregistry.PongPayload{
PingID: pingID,
Toolset: toolsetID,
ProviderID: providerID,
ProviderIncarnationID: incarnationID,
})
},
},
)
}
stream ID は決定的です:
- Tool calls:
toolset:<toolsetID>:requests - Results:
result:<toolUseID>
設定
Registry のオプション
ライブラリの例は最小限の設定を示しています。application が所有する Redis client を Redis に渡し、registry cluster で共有する Name を指定します。同じ名前と Redis database を使う node は、catalog、call record、health check の協調状態を共有します。各 node は Redis からコンパクトな catalog 状態を直接読み取り、完全な定義は別に保存します。
全 API と default 値は registry.Config を参照してください。PingInterval と MissedPingThreshold は health check、ExecutionTimeout は新しく受け付けた実行の時間上限、ResultStreamTTL は result の保持、ProviderLeaseDuration は provider 登録の更新を設定します。ExpectedToolsets は必要な catalog 名を telemetry に記録しますが、登録や call を拒否しません。Logger は call の確定処理の失敗を受け取ります。これらのオプションは registry の構築時に指定します。
Redis ストレージ
Redis の catalog は tool schema、admission identity、provider lease、health timestamp、登録撤回の履歴を保持します。call record と Pulse の request/result stream も Redis を使います。registry replica や再起動した process が同じ登録と call の判断を参照できるよう、永続化した Redis を使ってください。application が Redis client を所有し、registry の停止後に閉じます。
provider の更新とストレージの移行
provider は起動時に一度だけ生成済み schema を送信します。必須の Renew callback は、toolset、provider ID、incarnation ID、期待する登録 token を指定して RenewProvider を呼び、許可された lease 期間だけを返します。更新は lease を新規作成せず、token を変更せず、draining を解除しません。lease が消失、期限切れ、置換、撤回された場合は provider_lease_lost を返し、Serve が停止します。一時的な通信障害は既存 lease の制限時間内だけ再試行します。stream、consumer group、ping の協調状態の修復は引き続き利用できます。
registry は admission、lease、health、discovery のコンパクトな状態を、完全な定義と永久に撤回された token から分離して保存します。登録は関連するデータを原子的に更新します。health check と lease 操作はコンパクトな状態だけを転送します。各 process は fingerprint により定義を再利用し、コンパイル済み validator を保持します。Get と Resolve は呼び出し側ごとに独立した完全な値を返します。
以前の一体型 catalog からの移行には、計画的なメンテナンス停止が必要です。新しい作業を止め、受け付け済み call を完了させ、旧 registry が drain と lease 解放を処理できる間に provider を正常終了します。その後、旧 registry の書き込み process をすべて停止します。backup を取得して catalog を offline で変換し、すべての定義、identity、lease、timestamp、撤回済み token を保持します。call、stream、完了処理の index、保持期限は変更しません。新 registry と更新済み provider を起動し、厳密な起動時検証と正確な CheckAdmission が成功してから再開します。tool fingerprint と provider message version は変更されません。Renew callback を追加し、client を再生成してください。旧版と新版の writer を同時に動かしてはいけません。未変更の backup を復元できるのは、全 writer が停止し、新たな書き込みが再開される前だけです。再開後は新しい version で修復します。通常の起動は旧形式を受け付けません。
ヘルス監視
registry は Pulse stream で health ping を送信し、provider は gRPC の Pong メソッドで応答します。
仕組み
- health scheduler は共有 catalog から active toolset を読みます。
- toolset の ping lease を持つ node は、call を受け付ける有効な provider が存在する間、ping を送信します。
Pongは、応答が現在の登録、provider process、health check identity に一致する場合だけ catalog を更新します。- routing には、新しい call を受け付ける期限内の provider lease と、十分に新しい受理済み pong が必要です。
health は Redis の時刻を使って catalog から計算します。最後に受理した pong の経過時間は (MissedPingThreshold + 1) × PingInterval 以下である必要があります。未 publish の call は、既存の execution deadline までだけ healthy provider を待ちます。
分散協調
各 registry node はローカル scheduler を実行し、toolset ごとに有効期限付き Redis lease の取得を試みます。lease を取得した node がその health check を実行し、期限切れ後は別の node が取得できます。lease 名は registry cluster ごとに分かれています。
provider lease、現在の health check identity、最後に受理した pong は catalog の同じ record に保存されます。各 node はその record から health を計算するため、古い provider の遅れた応答によって現在の登録が healthy になることはありません。
クライアント統合
provider と invocation の API には、生成された registry service client を使います。catalog discovery では runtime/registry.NewClient が同じ生成 client を包み、ListToolsets、GetToolset、Search と、runtime/registry.Manager が使う resource type を提供します。
以下の例は catalog の一覧と、名前で指定した toolset の完全な schema を取得します。grpc.NewClient と deployment の認証情報で作成した connection を渡してください。呼び出し側が connection の所有権を保持します。上の provider の例と同じく、生成 client のすべての endpoint を接続しています。
package discovery
import (
"context"
genregistrygrpc "goa.design/goa-ai/registry/gen/grpc/registry/client"
genregistry "goa.design/goa-ai/registry/gen/registry"
runtimeregistry "goa.design/goa-ai/runtime/registry"
"google.golang.org/grpc"
)
// discoverTools lists the catalog and retrieves the schema of the named toolset.
// The caller creates the gRPC connection and keeps it open during discovery.
func discoverTools(ctx context.Context, conn *grpc.ClientConn, toolsetName string) (
[]*runtimeregistry.ToolsetInfo, *runtimeregistry.ToolsetSchema, error,
) {
transport := genregistrygrpc.NewClient(conn, grpc.WaitForReady(true))
generated := genregistry.NewClient(
transport.Register(),
transport.RenewProvider(),
transport.ReleaseProvider(),
transport.DrainProvider(),
transport.Unregister(),
transport.Pong(),
transport.RegisterAgentToolset(),
transport.ReplaceAgentToolset(),
transport.ListToolsets(),
transport.GetToolset(),
transport.ResolveToolset(),
transport.CheckAdmission(),
transport.Search(),
transport.CallTool(),
transport.CallResolvedTool(),
transport.RetryTool(),
transport.CompleteToolCall(),
transport.PublishToolOutputDelta(),
transport.ReportToolCallOverload(),
transport.ClaimToolCall(),
)
client := runtimeregistry.NewClient(generated)
toolsets, err := client.ListToolsets(ctx)
if err != nil {
return nil, nil, err
}
schema, err := client.GetToolset(ctx, toolsetName)
if err != nil {
return nil, nil, err
}
return toolsets, schema, nil
}
gRPC API
registry は次の gRPC method を公開します:
Provider Operations
| Method | 説明 |
|---|---|
Register | 起動時に生成済み定義を送信して provider を登録します。別の contract は古い lease の終了を待ちます。 |
RenewProvider | 期限内の正確な lease だけを、定義を送らずに延長します。draining と長い完了期限を維持し、権限を失った場合は provider_lease_lost を返します。 |
DrainProvider | 1 つの provider lease を新規 call に使えなくし、すでに所有する call を完了する権限は保ちます。 |
ReleaseProvider | process が受理済み work を確定した後、正確な provider lease を削除します。 |
Unregister | 正確な現在の登録を discovery から除外します。サービスの token は永久に廃止されますが、native 宣言は ReplaceAgentToolset で再有効化できます。受理済みの native な子呼び出しは選択した宣言を保持します。 |
Pong | 正確な current lease と health-check epoch に対して provider health を記録します。 |
ClaimToolCall | publish 済み request の実行権限を、正確に 1 つの provider lease に付与します。 |
CompleteToolCall | claim 済み call の正規 terminal result を commit し、result stream へ publish します。 |
PublishToolOutputDelta | claim 済み call の bounded な best-effort progress fragment を publish します。 |
ReportToolCallOverload | provider が overloaded call を実行する前に、bounded retry control を記録します。 |
native Agent の操作
| Method | 説明 |
|---|---|
RegisterAgentToolset | provider lease なしで native 宣言を登録します。有効な登録と同じ宣言を繰り返しても成功します。 |
ReplaceAgentToolset | 現在の token で native 宣言を置換または再有効化します。古い token には admission_conflict を返します。 |
Discovery Operations
| Method | 説明 |
|---|---|
ListToolsets | 登録済み toolset を一覧します (任意で tag filtering)。metadata のみを返し、full schema は返しません。 |
GetToolset | 指定 toolset の full schema を取得します。すべての tool input/output schema を含みます。 |
ResolveToolset | 有効な定義と正確な登録 token を一緒に読み取ります。provider の健全性は別に確認します。 |
CheckAdmission | 正確なサービス登録について、期限内で draining ではない provider lease と最近の health 応答を確認します。 |
Search | name、description、tags に対する keyword match で toolset を検索します。 |
Invocation Operations
| Method | 説明 |
|---|---|
CallTool | run 内で一意な call を検証して publish します。call は既存 deadline 内で provider health を待ち、publish 前だけ replacement に追随し、その後は正確で不変な execution reference を返します。 |
CallResolvedTool | 保持した登録 token のサービス呼び出しだけを発行します。発行前に置換されていれば call_not_admitted を記録します。 |
RetryTool | provider overload が記録された後、元の admission をそのまま再 publish します。replacement provider へ実行を移しません。 |
ベストプラクティス
デプロイ
- cluster 内のすべての node で同じ
Nameを使う: catalog と call の状態を共有し、health check を協調します - 同じ Redis instance を指す: state coordination のため
- load balancer の背後にデプロイする: すべての node が同一 state を返します
- catalog、call record、Pulse stream には永続化した Redis を使う: registry replica と再起動した process が同じ判断を参照できるようにします
ヘルス監視
PingIntervalとMissedPingThresholdを設定する: health check の頻度と許容する pong の経過時間を指定します。default 値はregistry.Configを参照してください。- catalog と health の telemetry を確認する: toolset が存在しない場合と、provider が現在 call を受け付けられない場合を区別します。
- execution deadline を維持する: 未 publish の call は既存の deadline までだけ provider の回復を待ちます。
スケーリング
- node を追加する: gRPC connection を増やしても、各 node は任意の request を処理できます
- node は health check を協調する: toolset ごとの有効期限付き Redis lease を使います
- sticky session は不要: result stream は Redis により cross-node delivery されるため、ある node で開始した tool call が別 node で完了できます
次のステップ
- Toolsets で tool の定義方法を学ぶ
- Production で deployment pattern を確認する
- Agent Composition で cross-agent tool sharing を理解する
現在のソース解決、生成済み契約、プロバイダー動作、移行についてはツール検索と動的カタログを参照してください。