Implementation Report · Kuma Dashboard
Gmail 業務ダッシュボードを「AI が後付け」から「AI と UI が同じアプリを操る」設計に作り替えた。 Builder.io が提唱する Agent-Native アーキテクチャの 5 原則を Gmail という小さな題材で一通り実装し、何が変わったかを記録する。
業務統合ダッシュボードから、Agent-Native アーキテクチャの実験場へ。
Kuma Dashboard は当初、Gmail / Slack / Calendar / Backlog / HubSpot を 1 つに集約する業務統合ダッシュボードとして始まった。 AI チャットは右の折りたたみパネルとして「補助的に」存在する、典型的な AI-enabled 構成だった。
プロジェクトの途中で、Builder.io の Vishwas Gopinath による Agent-Native: The Next Architecture for Software を読み、判別基準が刺さった:
「AI を取り除いてもプロダクトは動くか?」
Yes → AI-enabled(後付け)
No、UI なし → AI-native
No、UI あり → Agent-native — Builder.io ブログ
当時の Kuma は完全な AI-enabled だった。AI を切り離してもダッシュボードは動く。逆に言えば AI は本当の意味でアプリの中核ではなかった。
そこでスコープを変更した:
記事に書かれた設計原則を、Kuma に当てはめてどう実装したか。
| # | 原則 | 実装した内容 |
|---|---|---|
| 1 | Agent UI Parity UI でできることはエージェントもできる |
すべてのアクション(メール送信・下書き作成・遷移)が UI ボタン経由でも、チャット経由でも同じ実装を呼ぶ。 |
| 2 | Single Shared Action Model アクションを 1 回だけ定義 |
defineAction() で 1 か所に書き、Anthropic Tool / HTTP / MCP / UI が同じ定義を派生する。 |
| 3 | Shared State, Data, Context DB が UI と Agent の調整レイヤー |
ui_state テーブルを singleton として、UI からの状態 push と Agent からの遷移コマンドを DB 経由で同期。 |
| 4 | Protocol-Ready by Design MCP / A2A で外部公開 |
同じ registry を再利用して POST /api/mcp に JSON-RPC エンドポイントを公開。外部の Claude Code / Codex から到達可能。 |
| 5 | Governed Execution 権限スコープと監査 |
ソース × スコープのアクセス制御、書き込み操作の承認フラグ、全アクションの監査ログ、コスト/レイテンシ計測。 |
変更前は同じ仕様を 3〜4 か所に書いていた。変更後は 1 か所。
tools.ts に Anthropic Tool スキーマ手書き/api/gmail/reply ルートで入力検証を別途書くmcp/gmail.ts ハンドラで input parse を別途書くsrc/lib/actions/gmail/reply.ts に zod スキーマと run() を 1 回だけ書くexport const replyAction = defineAction({
name: "gmail_reply",
description: "メールスレッドに返信(draft / send 選択)",
input: z.object({
action: z.enum(["draft", "send"]),
to: z.string().min(1),
subject: z.string().min(1),
body: z.string().min(1),
thread_id: z.string().optional(),
in_reply_to: z.string().optional(),
cc: z.string().optional(),
}),
output: z.discriminatedUnion("status", [/* … */]),
scope: "gmail.write",
requiresConfirmation: true,
run: async (input, ctx) => {
// 唯一の実装ポイント
},
});
これ 1 つから派生するもの:
┌──────────────────────────────────┐
│ defineAction("gmail_reply") │
└──────┬───────────────────────────┘
│
┌───────────┼─────────────────┬──────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
Anthropic HTTP route MCP tool audit_log UI 確認
Tool 定義 /api/actions/ JSON-RPC 記録 フック
gmail_reply tools/call
同じ registry を外部エージェントへ JSON-RPC で公開。
POST /api/mcp で MCP サーバーを公開initialize / tools/list / tools/callmcpServers 設定にこの URL を追加するだけで全アクションが叩ける// ~/.claude.json
{
"mcpServers": {
"kuma": { "url": "http://localhost:3000/api/mcp" }
}
}
記事のコア:
アクションが共有単位になっていれば、プロトコル公開はルーティング問題に過ぎない。 — Builder.io ブログ
実際、MCP 公開のために書いたコードは route 1 ファイル(約 130 行)だけで、Action registry を読むコード以外は JSON-RPC のフレーミングだった。
「誰が、いつ、何を、どれくらいのコストで実行したか」を全部 DB に。
各アクションは scope: "gmail.read" | "gmail.write" | "ui.read" | "ui.write" を持ち、呼び出し元( agent / ui / http / mcp)×スコープのマトリクスで許可される。
| Source | gmail.read | gmail.write | ui.read | ui.write | 備考 |
|---|---|---|---|---|---|
| ui | ✓ | ✓ | ✓ | ✓ | ユーザー操作 |
| http | ✓ | ✓ | ✓ | ✓ | ローカル管理 API |
| agent | ✓ | ✓ | ✓ | ✓ | UI で確認フックを通す |
| mcp | ✓ | △ | ✓ | △ | write は allow_mcp_write 設定で動的切替 |
拒否されると、reason が action_log に "denied: …" として記録され、/audit ページで人間が確認できる。
すべてのアクション実行は action_log テーブルに自動記録される(読み取りスコープのポーリング由来はフィルタ済み)。/audit ページで時系列にフィルタ・展開できる。
Bedrock 呼び出しは chat_log テーブルに model / input_tokens / output_tokens / cost_usd / duration_ms / tool_rounds で記録。Sonnet 4.6 価格($3 / $15 per M tokens)でリアルタイム計算。/audit ページ上部に「直近 24h サマリ」を 5 カードで表示。
システムプロンプト・ユーザー追加指示は workspace_settings シングルトン行に保存。/settings ページから編集可能で、次のチャットから即反映される。ファイルでなく DBに置くことで、複数ユーザー・複数ワークスペースへの拡張が容易になる。
データ表示から「行動の context」への変化。
email_analysis テーブルにキャッシュ、再表示は即時「取引先 A 社の担当者から、打ち合わせ確認+サンプルデータ提供依頼への回答」というメールを開くと、Bedrock が以下のチップを生成:
✨ A 社担当者より、5/13(水)11:00〜の打ち合わせ承諾と
Web会議設定依頼、サンプルデータ提供は困難との回答。
[↩ Web会議URLを送付] [↩ サンプルデータ受領了承]
認証コードメールなら「195541 をコピー」、会議招待なら「日程調整を依頼」など、メールの種類に応じて UI 自体が変わる。 記事の「Runtime tools — エージェントがその場で作る私的ダッシュボード」の最小版として機能している。
コード規模・データベース・実装したアクション数で定量的に比較。
| # | 項目 | Before | After |
|---|---|---|---|
| 1 | アクションが 1 箇所に定義され、UI/Agent/API/CLI が同じ実装を呼ぶ | × | ✓ |
| 2 | Agent が「いまユーザーが見ている画面・選択・フィルタ」を取得できる | × | ✓ |
| 3 | Agent が UI をナビゲートできる | × | ✓ |
| 4 | 永続状態は DB に集約され、UI と Agent の両方が読み書きする | △ | ✓ |
| 5 | ワークスペース設定(指示・メモリ)がファイルでなく DB に保存される | × | ✓ |
| 6 | MCP サーバーとしてアプリの機能を公開できる | × | ✓ |
| 7 | Agent 権限がユーザー権限のサブセット以下 | × | ✓ |
| 8 | 操作ログ・監査証跡が人間に可視 | × | ✓ |
| 9 | 進捗・コスト・レイテンシが計測可能 | × | ✓ |
| 10 | 危険操作の承認フックを Agent も通る | × | ○ |
△=部分達成 / ○=部分達成(10 は requiresConfirmation フラグは Action に存在するが UI 側の確認モーダルと別実装のため)。
実装を終えて記憶に残った 5 つ。
UI と Agent の状態同期を、メッセージング・WebSocket・直接 RPC ではなく DB を介した宣言的な状態にしたことで、ポーリング 1 つで双方向に流れる単純な系になった。 レースコンディションは「最後に書いた人勝ち」で許容できる粒度に収まる。
Action を 1 か所に書く価値は、それを消費する surface の数に比例する。UI だけなら過剰、UI + Agent なら割に合う、UI + Agent + MCP なら劇的。MCP 公開を「ルーティング問題」に縮めたのが象徴的。
後付けで action_log を入れたが、ポーリングや内部 push がノイズになりやすい。
最初から shouldAudit(action, ctx) ルールを設計しておけばよかった。
agentVisible: false で十分
ui_state_update や workspace_settings_update は registry には入っているが、Anthropic Tool 派生時にフィルタしている。
「全部がツール」ではなく「誰に見せるかは別軸」というシンプルな2軸設計で運用できた。
Smart Action Bar はメール詳細から「Gmail バッジ」「カレンダー widget」「Chat ボタン」を削った後にようやく機能した。 AI が前に出るには UI が引かないと、ノイズになる。