Implementation Report · Kuma Dashboard

従来の実装と Agent-Native な実装の差分

Gmail 業務ダッシュボードを「AI が後付け」から「AI と UI が同じアプリを操る」設計に作り替えた。 Builder.io が提唱する Agent-Native アーキテクチャの 5 原則を Gmail という小さな題材で一通り実装し、何が変わったかを記録する。

Stack Next.js 14 · SQLite · Bedrock Sonnet 4.6 · zod Period 2026-04 〜 2026-05 Reference Builder.io "Agent-Native: The Next Architecture for Software"
01

プロジェクトピボットの経緯

業務統合ダッシュボードから、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 は本当の意味でアプリの中核ではなかった。

そこでスコープを変更した:

02

Agent-Native の 5 原則

記事に書かれた設計原則を、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
権限スコープと監査
ソース × スコープのアクセス制御、書き込み操作の承認フラグ、全アクションの監査ログ、コスト/レイテンシ計測。
03

単一アクションモデル

変更前は同じ仕様を 3〜4 か所に書いていた。変更後は 1 か所。

Before
  • tools.ts に Anthropic Tool スキーマ手書き
  • /api/gmail/reply ルートで入力検証を別途書く
  • mcp/gmail.ts ハンドラで input parse を別途書く
  • 同じ Gmail Reply 仕様が 3 か所に書かれており、変更時にドリフトが起こりがち
After
  • src/lib/actions/gmail/reply.ts に zod スキーマと run() を 1 回だけ書く
  • Anthropic Tool 配列・HTTP ルート・MCP ツール・監査ログがすべて自動派生
  • 1 ファイル変更すれば全 surface が追随

defineAction の例

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
派生される効果: アクションを 1 回定義するだけで、Anthropic Tool スキーマ・HTTP エンドポイント・MCP ツール・監査ログ・承認フックがすべて同期する。仕様変更時のドリフトリスクがゼロ。
04

UI と Agent の状態共有

「いま何を見ているか」「どこに遷移したいか」を DB 経由で双方向同期。

Before
  • Agent は Zustand store の中身を知らない
  • 「いま見ているメール」をリクエストごとにシステムプロンプトへ詰めていた(一方向 push)
  • Agent が UI を遷移させる手段なし
After
  • ui_state シングルトン行に currentRoute / selected / filters / pendingNav を集約
  • UI: ルート変更や選択変更で ui_state_updatepush
  • Agent: view_screenpullnavigate で遷移命令を書き込み
  • UI ポーリングが pendingNav を検出 → router.push() → クリア

同期パターン

  ┌─────────────────┐         ┌─────────────────┐
  │      UI         │         │     Agent       │
  └────────┬────────┘         └────────┬────────┘
           │                           │
   push    │ ui_state_update           │ view_screen
   route/  │                           │ → 読む
   選択    ▼                           │
       ┌───────────┐                   │
       │  ui_state │◄──── pendingNav ──┘
       │   (DB)    │           ─── navigate({route})
       └─────┬─────┘
             │
   pull      │ poll 2s
             ▼
       UI が pendingNav を検出 → router.push() → クリア

記事の言葉を借りれば「データベースが人間 UI とエージェントの調整レイヤー」になっている。 AgentがUIに直接 RPC せず、宣言的に DB へ書く → UI が観測して反応する設計。

05

プロトコル対応(MCP 公開)

同じ registry を外部エージェントへ JSON-RPC で公開。

Before
  • アプリは MCP クライアントとして外部 MCP に接続するだけ
  • 自分のアクションを外部から呼べない
After
  • POST /api/mcp で MCP サーバーを公開
  • methods: initialize / tools/list / tools/call
  • Claude Code・Codex 側の mcpServers 設定にこの URL を追加するだけで全アクションが叩ける
// ~/.claude.json
{
  "mcpServers": {
    "kuma": { "url": "http://localhost:3000/api/mcp" }
  }
}

記事のコア:

アクションが共有単位になっていれば、プロトコル公開はルーティング問題に過ぎない。 — Builder.io ブログ

実際、MCP 公開のために書いたコードは route 1 ファイル(約 130 行)だけで、Action registry を読むコード以外は JSON-RPC のフレーミングだった。

06

ガバナンス(権限・監査・コスト)

「誰が、いつ、何を、どれくらいのコストで実行したか」を全部 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 カードで表示。

ワークスペース設定の DB 化

システムプロンプト・ユーザー追加指示は workspace_settings シングルトン行に保存。/settings ページから編集可能で、次のチャットから即反映される。ファイルでなく DBに置くことで、複数ユーザー・複数ワークスペースへの拡張が容易になる。

07

UI そのものの変化

データ表示から「行動の context」への変化。

Before — データ表示
  • Inbox は受信メール一覧をテーブルで並べる
  • 各行に「Gmail」「Slack」のソースバッジ
  • 詳細はモーダルオーバーレイで表示、背景が暗転
  • AI に話しかけたい場合は別の右パネルを開く(コンテキスト切替)
After — 行動の context
  • Inbox は Gmail のみ。未読は左に glow 付きドット+件名太字、既読は灰色細字
  • 詳細パネルはsplit view(リスト常時表示・キーボード ↑↓ 切替)
  • 件名直下に ✨ AI 要約(1〜2 文、メール本文を読まずに判断できる)
  • その下に Smart Action Bar — メール内容から動的に生成されたチップ群(コピー / 返信下書き / アーカイブ / URL を開く)
  • 分析結果は email_analysis テーブルにキャッシュ、再表示は即時

Smart Action Bar の例

「取引先 A 社の担当者から、打ち合わせ確認+サンプルデータ提供依頼への回答」というメールを開くと、Bedrock が以下のチップを生成:

  ✨  A 社担当者より、5/13(水)11:00〜の打ち合わせ承諾と
      Web会議設定依頼、サンプルデータ提供は困難との回答。

  [↩ Web会議URLを送付]  [↩ サンプルデータ受領了承]

認証コードメールなら「195541 をコピー」、会議招待なら「日程調整を依頼」など、メールの種類に応じて UI 自体が変わる。 記事の「Runtime tools — エージェントがその場で作る私的ダッシュボード」の最小版として機能している。

08

数値で見る差分

コード規模・データベース・実装したアクション数で定量的に比較。

Actions registry
13
うち Gmail 9 / UI 5(書込確認: 6)
DB tables added
+5
action_log / chat_log / ui_state /
workspace_settings / email_analysis
Avg latency
<5ms
アクション実行(Bedrock 除く)
Cost / email analyze
$0.006
Sonnet 4.6 / キャッシュ後は無料

Agent-Native チェックリスト 10 項目

#項目BeforeAfter
1アクションが 1 箇所に定義され、UI/Agent/API/CLI が同じ実装を呼ぶ×
2Agent が「いまユーザーが見ている画面・選択・フィルタ」を取得できる×
3Agent が UI をナビゲートできる×
4永続状態は DB に集約され、UI と Agent の両方が読み書きする
5ワークスペース設定(指示・メモリ)がファイルでなく DB に保存される×
6MCP サーバーとしてアプリの機能を公開できる×
7Agent 権限がユーザー権限のサブセット以下×
8操作ログ・監査証跡が人間に可視×
9進捗・コスト・レイテンシが計測可能×
10危険操作の承認フックを Agent も通る×

△=部分達成 / ○=部分達成(10 は requiresConfirmation フラグは Action に存在するが UI 側の確認モーダルと別実装のため)。

09

学んだこと

実装を終えて記憶に残った 5 つ。

1. 「DB が調整レイヤー」は強い

UI と Agent の状態同期を、メッセージング・WebSocket・直接 RPC ではなく DB を介した宣言的な状態にしたことで、ポーリング 1 つで双方向に流れる単純な系になった。 レースコンディションは「最後に書いた人勝ち」で許容できる粒度に収まる。

2. 「単一定義」の威力は派生数に比例する

Action を 1 か所に書く価値は、それを消費する surface の数に比例する。UI だけなら過剰、UI + Agent なら割に合う、UI + Agent + MCP なら劇的。MCP 公開を「ルーティング問題」に縮めたのが象徴的。

3. 監査ログは早めに入れたほうがよかった

後付けで action_log を入れたが、ポーリングや内部 push がノイズになりやすい。 最初から shouldAudit(action, ctx) ルールを設計しておけばよかった。

4. Agent に見せない設定は agentVisible: false で十分

ui_state_updateworkspace_settings_update は registry には入っているが、Anthropic Tool 派生時にフィルタしている。 「全部がツール」ではなく「誰に見せるかは別軸」というシンプルな2軸設計で運用できた。

5. AI-native の "experience" は UI が薄くなることで生まれる

Smart Action Bar はメール詳細から「Gmail バッジ」「カレンダー widget」「Chat ボタン」を削った後にようやく機能した。 AI が前に出るには UI が引かないと、ノイズになる。

TL;DR — SaaS の形 + Agent の柔軟性 = Agent-Native。同じアプリモデルを人間と Agent の 2 つの surface で共有する。 アクションを 1 回だけ定義し、DB を調整レイヤーにし、MCP で外に開く。これがコア。