環境構築 — Antigravity + AgentKit 2.0
AntigravityとAgentKit 2.0を使ってA2A準拠エージェントを開発する環境を整えます。
# プロジェクト初期化
mkdir a2a-agent-system && cd a2a-agent-system
npm init -y
# AgentKit 2.0 と関連パッケージのインストール
npm install @google/agentkit@^2.0 @google/genai zod hono
# 開発用ツール
npm install -D typescript @types/node tsx nodemon
tsconfig.json の設定:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true
}
}
Antigravityのエージェント開発では、プロジェクトルートに .antigravity/rules.md を作成することで、A2A仕様への準拠ルールをコード補完に反映できます:
# A2Aエージェント開発ルール
- すべてのエージェントは /.well-known/agent.json を公開すること
- タスクIDはUUID v4を使用すること
- エラーレスポンスはA2A仕様のJSONRPCエラー形式に従うこと
- ストリーミングはSSE(Server-Sent Events)で実装すること
- 認証はBearer Token(JWT)を使用すること
エージェントカードの設計と実装
実際のA2Aエージェントを実装していきます。ここでは「コードレビューエージェント」を例に説明します。
エージェントカードの実装
// src/agent-card.ts
import { z } from "zod";
// エージェントカードのスキーマ定義
const AgentSkillSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
tags: z.array(z.string()),
inputModes: z.array(z.enum(["text", "file", "data"])),
outputModes: z.array(z.enum(["text", "file", "data"])),
examples: z.array(z.object({
name: z.string(),
description: z.string(),
input: z.record(z.unknown()),
})).optional(),
});
export const CODE_REVIEW_AGENT_CARD = {
name: "CodeReviewAgent",
description: "コードをレビューし、品質改善の提案を行うAIエージェント",
url: process.env.AGENT_BASE_URL ?? "http://localhost:3001",
version: "1.2.0",
documentationUrl: "https://your-docs.example.com/agents/code-review",
capabilities: {
streaming: true,
pushNotifications: true,
stateTransitionHistory: true,
},
authentication: {
schemes: ["Bearer"],
},
defaultInputMode: "text",
defaultOutputMode: "text",
skills: [
{
id: "review-code",
name: "コードレビュー",
description: "TypeScript/JavaScript/Pythonコードの品質レビューと改善提案",
tags: ["code-review", "quality", "typescript", "python"],
inputModes: ["text", "file"],
outputModes: ["text", "data"],
examples: [
{
name: "TypeScript関数レビュー",
description: "非同期関数のエラーハンドリングをレビューする",
input: { message: "この関数のエラーハンドリングを改善してください" },
},
],
},
{
id: "security-scan",
name: "セキュリティスキャン",
description: "一般的なセキュリティ脆弱性(XSS, SQLインジェクション等)の検出",
tags: ["security", "vulnerability", "owasp"],
inputModes: ["text", "file"],
outputModes: ["text", "data"],
},
],
} satisfies AgentCard;
// エージェントカード型定義
export type AgentCard = {
name: string;
description: string;
url: string;
version: string;
documentationUrl?: string;
capabilities: {
streaming: boolean;
pushNotifications: boolean;
stateTransitionHistory: boolean;
};
authentication: {
schemes: string[];
};
defaultInputMode: string;
defaultOutputMode: string;
skills: z.infer<typeof AgentSkillSchema>[];
};
Honoサーバーへの統合
// src/server.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { bearerAuth } from "hono/bearer-auth";
import { CODE_REVIEW_AGENT_CARD } from "./agent-card";
import { taskRouter } from "./routes/tasks";
const app = new Hono();
// CORS設定(A2Aクライアントからのクロスオリジンリクエストを許可)
app.use("*", cors({
origin: "*",
allowMethods: ["GET", "POST"],
allowHeaders: ["Content-Type", "Authorization"],
}));
// エージェントカードの公開(認証不要)
app.get("/.well-known/agent.json", (c) => {
return c.json(CODE_REVIEW_AGENT_CARD);
});
// タスクエンドポイント(認証必須)
app.use("/tasks/*", bearerAuth({ token: process.env.AGENT_TOKEN! }));
app.route("/tasks", taskRouter);
export default app;
タスク委譲パターンの実装
A2Aの真価はタスクの委譲にあります。3つの主要パターンを実装します。
パターン1: 同期タスク
処理が短時間(数秒以内)で完了する場合の実装です。
// src/routes/tasks.ts
import { Hono } from "hono";
import { z } from "zod";
import { GoogleGenerativeAI } from "@google/genai";
import { v4 as uuidv4 } from "uuid";
const taskRouter = new Hono();
const genai = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
// タスク送信スキーマ
const SendTaskSchema = z.object({
jsonrpc: z.literal("2.0"),
id: z.string().or(z.number()),
method: z.literal("tasks/send"),
params: z.object({
id: z.string().uuid(),
message: z.object({
role: z.literal("user"),
parts: z.array(z.object({
type: z.enum(["text", "file", "data"]),
text: z.string().optional(),
file: z.object({
name: z.string(),
mimeType: z.string(),
bytes: z.string(), // base64
}).optional(),
})),
}),
skillId: z.string().optional(),
}),
});
// tasks/send ハンドラ(同期)
taskRouter.post("/send", async (c) => {
const body = await c.req.json();
const parsed = SendTaskSchema.safeParse(body);
if (!parsed.success) {
return c.json({
jsonrpc: "2.0",
id: body.id ?? null,
error: {
code: -32600,
message: "Invalid Request",
data: parsed.error.flatten(),
},
}, 400);
}
const { params } = parsed.data;
const taskId = params.id;
const userMessage = params.message.parts
.filter(p => p.type === "text")
.map(p => p.text)
.join("\n");
try {
const model = genai.getGenerativeModel({ model: "gemini-2.5-pro" });
const result = await model.generateContent([
{
role: "user",
parts: [{
text: `あなたはプロのコードレビュアーです。以下のコードを分析してください:\n\n${userMessage}`,
}],
},
]);
const responseText = result.response.text();
return c.json({
jsonrpc: "2.0",
id: parsed.data.id,
result: {
id: taskId,
sessionId: uuidv4(),
status: {
state: "completed",
message: {
role: "agent",
parts: [{ type: "text", text: responseText }],
},
timestamp: new Date().toISOString(),
},
artifacts: [
{
name: "review-result",
description: "コードレビュー結果",
parts: [{ type: "text", text: responseText }],
},
],
},
});
} catch (error) {
return c.json({
jsonrpc: "2.0",
id: parsed.data.id,
error: {
code: -32603,
message: "Internal error",
data: error instanceof Error ? error.message : "Unknown error",
},
}, 500);
}
});
export { taskRouter };
パターン2: ストリーミングタスク
長時間かかるタスクでリアルタイムに結果を返す場合の実装です。SSE(Server-Sent Events)を使います。
// ストリーミングエンドポイント
taskRouter.post("/sendSubscribe", async (c) => {
const body = await c.req.json();
const taskId = body.params?.id ?? uuidv4();
// SSEレスポンスのセットアップ
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const sendEvent = (data: unknown) => {
const payload = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(payload));
};
try {
// 作業開始を通知
sendEvent({
jsonrpc: "2.0",
id: body.id,
result: {
id: taskId,
status: {
state: "working",
message: {
role: "agent",
parts: [{ type: "text", text: "コードを解析中..." }],
},
timestamp: new Date().toISOString(),
},
},
});
const model = genai.getGenerativeModel({ model: "gemini-2.5-pro" });
const userMessage = body.params?.message?.parts
?.filter((p: { type: string }) => p.type === "text")
.map((p: { text: string }) => p.text)
.join("\n") ?? "";
// ストリーミング生成
const streamResult = await model.generateContentStream([
{
role: "user",
parts: [{ text: `コードをレビューしてください:\n\n${userMessage}` }],
},
]);
let fullText = "";
let chunkIndex = 0;
for await (const chunk of streamResult.stream) {
const chunkText = chunk.text();
fullText += chunkText;
// 中間アーティファクトを送信
sendEvent({
jsonrpc: "2.0",
id: body.id,
result: {
id: taskId,
status: {
state: "working",
timestamp: new Date().toISOString(),
},
artifact: {
name: "review-in-progress",
index: chunkIndex++,
append: chunkIndex > 1,
lastChunk: false,
parts: [{ type: "text", text: chunkText }],
},
},
});
}
// 完了を通知
sendEvent({
jsonrpc: "2.0",
id: body.id,
result: {
id: taskId,
status: {
state: "completed",
timestamp: new Date().toISOString(),
},
artifact: {
name: "review-complete",
index: chunkIndex,
lastChunk: true,
parts: [{ type: "text", text: "" }], // 最終チャンクは空
},
},
});
controller.close();
} catch (error) {
sendEvent({
jsonrpc: "2.0",
id: body.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : "Stream error",
},
});
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
});
A2Aクライアントの実装 — 別エージェントからの呼び出し
エージェントが別のエージェントを呼び出す側(クライアント側)の実装です。これによって真のマルチエージェントシステムが完成します。
// src/a2a-client.ts
export class A2AClient {
private agentUrl: string;
private token: string;
constructor(agentUrl: string, token: string) {
this.agentUrl = agentUrl.replace(/\/$/, "");
this.token = token;
}
/** エージェントカードを取得して能力を確認する */
async getAgentCard() {
const res = await fetch(`${this.agentUrl}/.well-known/agent.json`);
if (!res.ok) throw new Error(`Agent card fetch failed: ${res.status}`);
return res.json();
}
/** 同期タスク送信 */
async sendTask(params: {
taskId: string;
message: string;
skillId?: string;
}) {
const body = {
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tasks/send",
params: {
id: params.taskId,
message: {
role: "user",
parts: [{ type: "text", text: params.message }],
},
skillId: params.skillId,
},
};
const res = await fetch(`${this.agentUrl}/tasks/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`Task failed: ${res.status} - ${JSON.stringify(err)}`);
}
const result = await res.json();
if (result.error) {
throw new Error(`A2A Error ${result.error.code}: ${result.error.message}`);
}
return result.result;
}
/** ストリーミングタスク送信(非同期イテレータを返す) */
async *sendTaskStream(params: {
taskId: string;
message: string;
}): AsyncGenerator<unknown> {
const body = {
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tasks/sendSubscribe",
params: {
id: params.taskId,
message: {
role: "user",
parts: [{ type: "text", text: params.message }],
},
},
};
const res = await fetch(`${this.agentUrl}/tasks/sendSubscribe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify(body),
});
if (!res.ok || !res.body) throw new Error("Stream init failed");
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const json = JSON.parse(line.slice(6));
yield json;
}
}
}
}
}
// 使用例:オーケストレーターエージェントからコードレビューエージェントを呼ぶ
async function orchestrateCodeReview(code: string) {
const client = new A2AClient(
"https://code-review-agent.example.com",
process.env.AGENT_TOKEN!
);
// エージェントの能力を確認
const card = await client.getAgentCard();
console.log(`接続先エージェント: ${card.name} v${card.version}`);
console.log(`対応スキル: ${card.skills.map((s: { name: string }) => s.name).join(", ")}`);
// ストリーミングでコードレビューを実行
const taskId = crypto.randomUUID();
process.stdout.write("レビュー結果: ");
for await (const event of client.sendTaskStream({ taskId, message: code })) {
const result = (event as { result?: { artifact?: { parts?: Array<{ type: string; text: string }> } } }).result;
if (result?.artifact?.parts) {
for (const part of result.artifact.parts) {
if (part.type === "text" && part.text) {
process.stdout.write(part.text);
}
}
}
}
console.log("\n✅ レビュー完了");
}
認証とセキュリティの実装
本番環境では、エージェント間通信のセキュリティが不可欠です。
JWTベースの認証
// src/auth.ts
import { SignJWT, jwtVerify } from "jose";
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
/** エージェント間通信用のトークンを発行する */
export async function issueAgentToken(params: {
issuerId: string; // 発行元エージェントID
targetId: string; // 宛先エージェントID
scopes: string[]; // 許可する操作
expiresIn?: number; // 有効期限(秒)デフォルト3600
}): Promise<string> {
return new SignJWT({
iss: params.issuerId,
aud: params.targetId,
scopes: params.scopes,
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(`${params.expiresIn ?? 3600}s`)
.sign(SECRET);
}
/** トークンを検証し、スコープをチェックする */
export async function verifyAgentToken(
token: string,
requiredScope: string,
audienceId: string
): Promise<{ issuerId: string; scopes: string[] }> {
const { payload } = await jwtVerify(token, SECRET, {
audience: audienceId,
});
const scopes = (payload.scopes as string[]) ?? [];
if (!scopes.includes(requiredScope)) {
throw new Error(`Insufficient scope. Required: ${requiredScope}`);
}
return { issuerId: payload.iss as string, scopes };
}
レートリミットの実装
// src/middleware/rate-limit.ts
import { MiddlewareHandler } from "hono";
// シンプルなインメモリレートリミッター(本番ではRedis/Upstashを推奨)
const requestCounts = new Map<string, { count: number; resetAt: number }>();
export const rateLimitMiddleware = (
maxRequests = 60,
windowMs = 60_000
): MiddlewareHandler => {
return async (c, next) => {
const clientId = c.req.header("Authorization")?.slice(7) ?? c.req.header("X-Agent-ID") ?? "anonymous";
const now = Date.now();
const record = requestCounts.get(clientId);
if (!record || now > record.resetAt) {
requestCounts.set(clientId, { count: 1, resetAt: now + windowMs });
c.header("X-RateLimit-Limit", String(maxRequests));
c.header("X-RateLimit-Remaining", String(maxRequests - 1));
return next();
}
if (record.count >= maxRequests) {
return c.json({
jsonrpc: "2.0",
id: null,
error: {
code: -32000,
message: "Rate limit exceeded",
data: { retryAfter: Math.ceil((record.resetAt - now) / 1000) },
},
}, 429);
}
record.count++;
c.header("X-RateLimit-Limit", String(maxRequests));
c.header("X-RateLimit-Remaining", String(maxRequests - record.count));
return next();
};
};
エラーハンドリングとリトライ戦略
A2Aシステムでの堅牢なエラーハンドリングは、本番運用の鍵となります。
A2A標準エラーコード
A2Aプロトコルは以下のJSONRPCエラーコードを標準化しています:
- -32700: Parse error(JSONパース失敗)
- -32600: Invalid Request(必須フィールド欠落)
- -32601: Method not found(未対応のメソッド)
- -32602: Invalid params(パラメータ不正)
- -32603: Internal error(サーバー内部エラー)
- -32001: Task not found(指定IDのタスクなし)
- -32002: Task not cancelable(キャンセル不可能なタスク)
- -32003: Push notification not supported(プッシュ通知非対応)
- -32004: Unsupported operation(非対応オペレーション)
- -32005: Incompatible content types(非対応コンテンツタイプ)
指数バックオフ付きリトライクライアント
// src/retry-client.ts
type RetryOptions = {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
retryableErrors?: number[]; // リトライ対象のエラーコード
};
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 500,
maxDelayMs = 10_000,
retryableErrors = [-32603], // デフォルトはInternal errorのみ
} = options;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// エラーコードを解析してリトライ可否を判断
const match = lastError.message.match(/A2A Error (-?\d+):/);
const errorCode = match ? parseInt(match[1], 10) : -32603;
if (!retryableErrors.includes(errorCode)) {
throw lastError; // リトライ不可能なエラーは即座に再スロー
}
if (attempt < maxAttempts) {
// 指数バックオフ + ジッター
const delay = Math.min(
initialDelayMs * Math.pow(2, attempt - 1) + Math.random() * 100,
maxDelayMs
);
console.warn(`Attempt ${attempt}/${maxAttempts} failed. Retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError ?? new Error("Max retry attempts exceeded");
}
// 使用例
const result = await withRetry(
() => client.sendTask({ taskId: crypto.randomUUID(), message: "コードをレビューして" }),
{ maxAttempts: 3, retryableErrors: [-32603, -32000] }
);
Cloudflare Workers へのデプロイ
AntigravityとCloudflare Workersの相性は抜群です。A2Aエージェントをエッジにデプロイする手順を説明します。
// src/worker.ts
import { Hono } from "hono";
import { CODE_REVIEW_AGENT_CARD } from "./agent-card";
import { taskRouter } from "./routes/tasks";
const app = new Hono<{ Bindings: Env }>();
// Agent Card
app.get("/.well-known/agent.json", (c) => {
const card = {
...CODE_REVIEW_AGENT_CARD,
url: `https://${new URL(c.req.url).hostname}`,
};
return c.json(card, 200, {
"Cache-Control": "public, max-age=3600",
});
});
app.route("/tasks", taskRouter);
export default app;
wrangler.toml の設定:
name = "code-review-agent"
main = "src/worker.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
AGENT_BASE_URL = "https://code-review-agent.your-subdomain.workers.dev"
[[secrets]]
name = "GOOGLE_API_KEY"
[[secrets]]
name = "JWT_SECRET"
[[secrets]]
name = "AGENT_TOKEN"
デプロイコマンド:
# シークレットを設定
wrangler secret put GOOGLE_API_KEY
wrangler secret put JWT_SECRET
wrangler secret put AGENT_TOKEN
# デプロイ
wrangler deploy
# Agent Cardの確認
curl https://code-review-agent.your-subdomain.workers.dev/.well-known/agent.json | jq .
実践ユースケース — コードレビュー + ドキュメント生成パイプライン
複数のA2Aエージェントを連携させた実践的なユースケースを示します。
[オーケストレーター]
│
├──→ [コードレビューエージェント] → レビュー結果
│
├──→ [ドキュメント生成エージェント] → API仕様書
│
└──→ [テスト生成エージェント] → ユニットテスト
// src/orchestrator.ts
import { A2AClient } from "./a2a-client";
import { withRetry } from "./retry-client";
export async function runCodePipeline(sourceCode: string) {
const reviewClient = new A2AClient(
process.env.REVIEW_AGENT_URL!,
process.env.REVIEW_AGENT_TOKEN!
);
const docClient = new A2AClient(
process.env.DOC_AGENT_URL!,
process.env.DOC_AGENT_TOKEN!
);
// エージェントの能力を事前に確認
const [reviewCard, docCard] = await Promise.all([
reviewClient.getAgentCard(),
docClient.getAgentCard(),
]);
console.log(`✓ ${reviewCard.name} (${reviewCard.version}) - 接続OK`);
console.log(`✓ ${docCard.name} (${docCard.version}) - 接続OK`);
// 並列でタスクを実行(互いに依存しないタスクは並列化)
const [reviewResult, docResult] = await Promise.all([
withRetry(() => reviewClient.sendTask({
taskId: crypto.randomUUID(),
message: `以下のコードをレビューしてください:\n\`\`\`typescript\n${sourceCode}\n\`\`\``,
skillId: "review-code",
})),
withRetry(() => docClient.sendTask({
taskId: crypto.randomUUID(),
message: `以下のコードのJSDocコメントとREADMEを生成してください:\n\`\`\`typescript\n${sourceCode}\n\`\`\``,
})),
]);
return {
review: reviewResult.artifacts?.[0]?.parts?.[0]?.text ?? "",
documentation: docResult.artifacts?.[0]?.parts?.[0]?.text ?? "",
};
}
このパイプラインでは、コードレビューとドキュメント生成を並列実行しているため、逐次実行と比べて処理時間を最大50%短縮できます。Antigravity IDEのエージェントモードで上記を開発する際は、「並列タスクはPromise.allで」というルールをrules.mdに追記しておくと、コード生成時に自動的に適用されます。
マルチエージェントシステムの基礎については AgentKit 2.0 プロダクション実装パターン も参考にしてください。また、MCPとの連携については カスタムMCPサーバー プロダクションガイド で詳しく解説しています。
規模が大きくなったときの設計 — タスクルーティングとエージェント探索
A2Aシステムが育ってくると、オーケストレーター側にエージェントのURLをハードコードする方式は保守の負担になってきます。ここでは動的なエージェント探索を解決する、軽量なエージェントレジストリのパターンを紹介します。
エージェントレジストリを実装する
// src/agent-registry.ts
type AgentRegistration = {
url: string;
token: string;
card?: AgentCard;
lastSeen: Date;
healthy: boolean;
};
export class AgentRegistry {
private agents = new Map<string, AgentRegistration>();
register(name: string, url: string, token: string) {
this.agents.set(name, { url, token, lastSeen: new Date(), healthy: true });
}
async resolve(name: string): Promise<{ url: string; token: string; card: AgentCard }> {
const registration = this.agents.get(name);
if (!registration) throw new Error(`未登録のエージェントです: ${name}`);
if (!registration.healthy) throw new Error(`エージェントが不健全な状態です: ${name}`);
// 初回アクセス時にエージェントカードを取得してキャッシュする
if (!registration.card) {
const res = await fetch(`${registration.url}/.well-known/agent.json`);
registration.card = await res.json();
}
return { url: registration.url, token: registration.token, card: registration.card! };
}
/** 指定したスキルIDをサポートするエージェントを探す */
async findBySkill(skillId: string): Promise<string[]> {
const matches: string[] = [];
for (const [name, reg] of this.agents.entries()) {
if (!reg.card) {
try {
const res = await fetch(`${reg.url}/.well-known/agent.json`);
reg.card = await res.json();
} catch {
reg.healthy = false;
continue;
}
}
const hasSkill = reg.card?.skills?.some(s => s.id === skillId);
if (hasSkill) matches.push(name);
}
return matches;
}
/** 定期ヘルスチェック — 応答しないエージェントを不健全とマークする */
async healthCheck() {
const checks = Array.from(this.agents.entries()).map(async ([name, reg]) => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(`${reg.url}/.well-known/agent.json`, {
signal: controller.signal,
});
clearTimeout(timeout);
reg.healthy = res.ok;
reg.lastSeen = new Date();
} catch {
reg.healthy = false;
}
return { name, healthy: reg.healthy };
});
return Promise.all(checks);
}
}
// オーケストレーターでの利用例
const registry = new AgentRegistry();
registry.register("code-review", process.env.REVIEW_AGENT_URL!, process.env.REVIEW_AGENT_TOKEN!);
registry.register("documentation", process.env.DOC_AGENT_URL!, process.env.DOC_AGENT_TOKEN!);
registry.register("test-generator", process.env.TEST_AGENT_URL!, process.env.TEST_AGENT_TOKEN!);
// スキルを指定して動的にルーティングする
const reviewAgents = await registry.findBySkill("review-code");
console.log(`review-code スキルを持つエージェントが ${reviewAgents.length} 件見つかりました`);
このレジストリの良いところは、エージェントを増やすときにオーケストレーターのコードを書き換えなくて済む点です。新しいエージェントを register するだけで、findBySkill 経由で自動的に探索対象に入ります。
スキル単位の負荷分散
同じスキルを複数のエージェントが提供している場合(たとえば冗長化のためにコードレビューエージェントを3インスタンス動かしているとき)、ラウンドロビンや最小負荷ルーティングでホットスポットを防げます。
// src/load-balancer.ts
export class RoundRobinBalancer {
private counters = new Map<string, number>();
next(candidates: string[]): string {
if (candidates.length === 0) throw new Error("候補となるエージェントがありません");
const key = candidates.join("|");
const current = this.counters.get(key) ?? 0;
const selected = candidates[current % candidates.length];
this.counters.set(key, current + 1);
return selected;
}
}
// オーケストレーター内で、スキルに対して最も空いているエージェントを選ぶ
const balancer = new RoundRobinBalancer();
async function delegateToSkill(skillId: string, message: string) {
const candidates = await registry.findBySkill(skillId);
if (candidates.length === 0) throw new Error(`このスキルに対応するエージェントがありません: ${skillId}`);
const agentName = balancer.next(candidates);
const { url, token } = await registry.resolve(agentName);
const client = new A2AClient(url, token);
return client.sendTask({ taskId: crypto.randomUUID(), message, skillId });
}
パフォーマンスの実測値とチューニング
性能特性を把握しておくと、アーキテクチャ上のトレードオフを正しく判断できます。
レイテンシの内訳
典型的なA2A呼び出しのレイテンシは、大きく3つの要素に分解できます。
- ネットワークのオーバーヘッド: HTTPリクエスト・レスポンスの往復で、同一リージョンのWorkers同士なら概ね20〜100ミリ秒
- エージェントカードの取得: エージェントごとにセッション内で一度だけ。初回取得後はキャッシュする
- タスク処理そのもの: モデルとプロンプトの複雑さに完全に依存する
本記事のコードレビューのユースケースでは、私の手元では概ね次のような値になりました。
- エージェントカード取得(初回): 約50ミリ秒
- 同期タスク(単純な関数・約200トークン): 約800ミリ秒〜1.2秒
- ストリーミングタスク(複雑なモジュール・約2,000トークン): 最初のチャンクまで2〜4秒、完了まで8〜12秒
チューニングの指針
1. 独立したタスクは積極的に並列化する。 オーケストレーターの例で示したとおり、Promise.all が最も効果の大きい最適化です。パイプラインの中に、互いに依存関係のない複数エージェントへのファンアウトがあるなら、それらは必ず並列で走らせるべきです。
2. エージェントカードはTTL付きでキャッシュする。 エージェントカードのメタデータはめったに変わりません。1時間程度のTTLでキャッシュしておけば、タスクのたびに発生する余計なHTTP往復を省けます。
const cardCache = new Map<string, { card: AgentCard; expiresAt: number }>();
async function getCachedAgentCard(url: string, ttlMs = 3_600_000): Promise<AgentCard> {
const cached = cardCache.get(url);
if (cached && Date.now() < cached.expiresAt) return cached.card;
const res = await fetch(`${url}/.well-known/agent.json`);
const card = await res.json() as AgentCard;
cardCache.set(url, { card, expiresAt: Date.now() + ttlMs });
return card;
}
3. 500ミリ秒を超えるタスクはストリーミングにする。 半秒以上かかるタスクはストリーミングの恩恵を受けられます。UIの応答性が保たれ、ユーザーはスピナーを眺める代わりに進捗を確認できます。
4. リトライのパラメータはエラーコードごとに調整する。 すべてのエラーが同じリトライ挙動でよいわけではありません。レート制限エラー(-32000)は強めにバックオフ(5〜30秒)し、内部エラー(-32603)は短い間隔(500ミリ秒〜2秒)で再試行、ネットワークエラーはユーザーに見せる前に2〜3回再試行するのが目安です。
5. エージェントカードは小さく絞る。 エージェントカードのスキルが増えるほど、オーケストレーターがルーティング先を評価する探索コストが上がります。20個のスキルを抱えた巨大なエージェントを1つ作るより、3〜5個に絞ったエージェントを複数用意するほうが扱いやすくなります。
つまずきやすいポイントと回避策
A2Aシステムを初めて作ると、いくつか繰り返し起きがちな失敗があります。先に知っておくと避けやすいものを挙げておきます。
落とし穴1: JWTに aud を設定し忘れる。 JWTが対象オーディエンス(aud)を指定していないと、漏洩したトークンがシステム内のどのエージェントに対しても再利用できてしまいます。必ず aud: targetAgentId を含め、受信側で検証してください。
落とし穴2: 部分的なストリーミングアーティファクトを扱わない。 接続が切れたあとにストリーミングタスクへ再接続すると、クライアントがチャンクを重複して受け取ることがあります。artifact.index を追跡し、最後に正常処理したインデックスより小さいチャンクはスキップしましょう。
落とし穴3: 異なるタスクに同じタスクIDを使う。 タスクIDは論理的な作業単位ごとに一意であるべきです。リトライで同じタスクIDを使うのは冪等性の観点から問題ありません(むしろ意図的です)が、意味的に別のリクエストに使い回すと、タスク状態をキャッシュするエージェントで予測不能な挙動を招きます。
落とし穴4: キャッシュヘッダーなしでエージェントカードを配信する。 Cache-Control: public, max-age=3600 を付けないと、エージェント探索のたびにWorkerの計算リソースを消費します。多数のオーケストレーターが利用可能なエージェントをポーリングする状況では、これがあっという間に積み上がります。
落とし穴5: ヘルスチェックの頻度が高すぎる。 20エージェントに対して5秒ごとにヘルスチェックを走らせるような積極的すぎる監視は、不要な負荷を生みます。多くの本番システムでは、30秒間隔・タイムアウト5秒あたりが妥当な出発点です。
運用してわかったこと — 標準プロトコルが効いてくる場面
私が最初に「標準仕様で異なる相手とつながる」感覚に触れたのは、1997年、16歳でインターネットに出会ったときでした。独学で覚えたプログラミングと、誰が決めたともいえる共通の通信規約だけを頼りに、国境を越えて見知らぬ人とやり取りできたあの体験は、今でも技術選定の基準のどこかに残っています。A2Aプロトコルが掲げる「異なるフレームワーク・異なる言語で書かれたエージェント同士が、ひとつの標準インターフェースで会話する」という思想は、まさにあの頃感じた開放感の延長線上にあると感じています。
実務的な話をすると、私は個人開発のアプリ事業(累計5,000万ダウンロード規模)で、バックエンドの非同期ジョブを複数のワーカーに委譲する設計を長く運用してきました。そこで一番こたえたのは、機能そのものよりも「自前で決めた独自プロトコルの保守コスト」でした。送信側と受信側のスキーマがいつの間にかずれ、片方を直すともう片方が壊れる。A2Aのように、能力の宣言(エージェントカード)・タスクの受け渡し・エラーコードまでが標準化されていることの価値は、こうした地味な保守の現場でこそ効いてきます。
運用してみて、ドキュメントだけでは気づきにくかった判断軸を3つ挙げておきます。ひとつ目は、エージェントカードのキャッシュは「速度のため」だと思われがちですが、実際には障害の切り分けを楽にするという点です。カードを一度キャッシュしておくと、タスクが失敗したときに「探索で落ちたのか、処理で落ちたのか」をログから即座に分離できます。ふたつ目は、ヘルスチェックの間隔は、エージェント数ではなく「タスクの平均所要時間」を基準に決めるとうまくいくこと。長時間タスクが多い構成で5秒間隔のチェックを回すと、健全なエージェントまで一時的にタイムアウトで不健全判定されてしまいます。みっつ目は、最初から完璧な探索機構を作り込まないこと。エージェントが3〜4個のうちは、本記事のレジストリくらいの軽さで十分で、要らない抽象化は後から自分の首を絞めます。
まとめと次の一歩
A2AプロトコルをAntigravityと組み合わせると、エージェントカードの設計から、タスク委譲、認証、レジストリによる動的探索、そしてCloudflare Workersへのデプロイまでを、ひとつの一貫した設計思想でつなげられます。本記事で繰り返し効いてきたのは、Promise.all による並列化と、エージェントカードのキャッシュという、派手さはないけれど効果の大きい2つの判断でした。
次の一歩としては、まず本記事のコードレビューエージェントを1つだけ立て、/.well-known/agent.json を返すところまでを動かしてみてください。エージェントカードが正しく公開できれば、残りのタスク委譲やレジストリは、その土台の上に少しずつ積み上げていけます。最初から分散システム全体を設計しようとせず、1エージェントの往復が通る瞬間を作ることが、いちばんの近道だと感じています。
実装の参考になれば幸いです。お読みいただきありがとうございました。