コンテンツにスキップ

STUDY NOTES

第32回: deepagents + CopilotKit で arXiv 論文 → スライド生成エージェント(TypeScript / Next.js)

連載が初めて Python 以外(TypeScript + Next.js + Bun)で書かれる回。3 つの新スタック:

ライブラリ 役割 立ち位置
deepagents 「Skill ベース」のディープエージェントを LangGraph 上で構築 LangChain 公式の create_agent の TypeScript 版 + Skill 機能
@copilotkit/react-core + runtime React フロントエンド ↔ エージェント間の状態同期と UI コンポーネント Vercel AI SDK / useChat の高機能版
langchain-copilotkit(連載著者の自作) deepagents エージェント ↔ CopilotRuntime を繋ぐアダプタ 2 つの生態系を接続する hub

サンプルは 「arXiv 論文 URL を貼ると、エージェントが論文を読み込み → アウトライン提案 → 対話で修正 → PowerPoint (.pptx) を生成してダウンロードリンク」を作るブラウザアプリ。React UI と LangGraph エージェントが双方向にデータを流す点が肝。

大きな違い (Python の連載回 vs 32 回): - 第29-31回の Streamlit は 「Python が描画も担当」だったので React コンポーネントは出てこなかった - 32 回は エージェントが Server Component、UI が Client Component に分離 → 本物の Web アプリとして動く - エージェントは 「Skill (Markdown ファイル) + memory (AGENTS.md) + 仮想シェル」でツールではなくスキルとしてワークフローを持つ


全体像

32/
├── package.json                          ← bun + Next.js 16 + React 19 + deepagents
├── Dockerfile / docker-compose.yml       ← 推奨実行環境(LocalShellBackend のリスク回避)
├── biome.jsonc / knip.jsonc              ← Lint + 未使用 export 検知
├── agent/                                ← LangGraph エージェント側
│   ├── agent.ts                          ← createDeepAgent + LocalShellBackend
│   ├── generate-pptx-tool.ts             ← Zod スキーマ検証 + JSON 読み込みツール
│   └── system-prompt.ts                  ← エージェント基本方針
├── app/                                  ← Next.js App Router
│   ├── layout.tsx                        ← CopilotKit プロバイダ
│   ├── page.tsx                          ← 2 カラム UI (プレビュー + チャット)
│   ├── api/copilotkit/route.ts           ← CopilotRuntime エンドポイント
│   └── components/
│       ├── slide-preview.tsx
│       ├── slide-context.tsx             ← React Context でスライドデータ共有
│       └── tool-call-renderer.tsx        ← ツール実行UI + ★PptxGenJS で .pptx 生成
└── workspace/                            ← エージェントの作業ディレクトリ(virtualMode で sandbox)
    ├── AGENTS.md                         ← エージェントメモリ
    └── .agent/skills/pptx-generator/SKILL.md  ← スキル定義(ワークフロー手順)

アプリ全体のシーケンス:

sequenceDiagram
    participant User
    participant UI as page.tsx<br/>(CopilotChat)
    participant Runtime as CopilotRuntime<br/>(/api/copilotkit)
    participant Adapter as LangChainAgentAdapter
    participant Agent as createSlideAgent<br/>(deepagents)
    participant Shell as LocalShellBackend<br/>(virtualMode: workspace/)
    participant arXiv
    participant Tool as generate_pptx tool
    participant Renderer as ToolCallRenderer<br/>(client)

    User->>UI: arXiv URL ペースト
    UI->>Runtime: messages 送信
    Runtime->>Adapter: streamEvents
    Adapter->>Agent: invoke

    Note over Agent: SKILL.md を読み込む

    Agent->>Shell: curl https://arxiv.org/abs/{id}
    Shell->>arXiv: GET
    arXiv-->>Shell: HTML
    Shell-->>Agent: 論文ページ
    Agent->>Shell: curl https://arxiv.org/html/{id}
    Shell->>arXiv: GET
    arXiv-->>Shell: HTML 本文
    Shell-->>Agent: 本文

    Note over Agent: タイトル/著者/セクションを抽出

    Agent->>Shell: write ./slides/{id}.json
    Agent-->>Adapter: ダイジェスト(テーブル形式)
    Adapter-->>Runtime: streaming
    Runtime-->>UI: アウトライン表示
    UI->>User: 確認待ち

    User->>UI: 「3枚目を XX に変えて」
    UI->>Runtime: messages
    Runtime->>Agent: invoke
    Agent->>Shell: read ./slides/{id}.json
    Agent->>Shell: write 修正済み JSON
    Agent-->>UI: 修正後ダイジェスト

    User->>UI: 「OK」
    UI->>Runtime: messages
    Runtime->>Agent: invoke
    Agent->>Tool: generate_pptx({ filePath })
    Tool->>Shell: readRaw(filePath)
    Shell-->>Tool: JSON 内容
    Tool->>Tool: Zod safeParse で検証
    Tool-->>Agent: { success: true, slides: [...] }
    Agent-->>Adapter: ツール完了
    Adapter-->>Runtime: tool result
    Runtime-->>Renderer: ToolCallRenderer に props として届く
    Renderer->>Renderer: PptxGenJS で .pptx を base64 生成
    Renderer-->>User: ダウンロードボタン表示

ポイント: generate_pptx ツールはサーバ側では JSON 検証だけ。実際の .pptx バイナリ生成は ブラウザ側の PptxGenJS で行う。Node.js に重い PPTX 生成依存を入れない + 生成済みファイルがブラウザに即ダウンロードできる、という賢い分業。


使用ライブラリ・原理

deepagents (createDeepAgent) — Skill ベースのエージェント構築
import { LocalShellBackend, createDeepAgent } from "deepagents";

const agent = createDeepAgent({
  model,
  systemPrompt: SYSTEM_PROMPT,
  tools: [createGeneratePptxTool(backend)],
  skills: ["./.agent/skills/"],           // ★ Markdown でスキル定義
  memory: ["./AGENTS.md"],                // ★ エージェントメモリ
  backend,
  checkpointer: new MemorySaver(),
});

deepagents の特徴:

  • Skill = SKILL.md Markdown ファイル。各スキルは「frontmatter(name, description)+ 本文(手順、ガイドライン)」で構成
  • Memory = AGENTS.md Markdown ファイル。長期記憶として全タスクで参照される
  • Backend = ツール(shell, file system)を実行する抽象化レイヤLocalShellBackend 以外に Docker / Kubernetes / E2B などへ差し替え可能
  • 内部実装は LangGraph + create_agent + 自動 SKILL/AGENTS.md 注入

Claude Code との対比: deepagents の SKILL.md は Claude Code の Skill 機能と同じファイル構造(frontmatter + Markdown)。実は Claude Code Skill の OSS 実装としても使える。memory も Claude Code の auto-memory に対応概念。

LocalShellBackend(virtualMode: true) — ホスト直接実行 + workspace sandbox
const backend = new LocalShellBackend({
  rootDir: workspaceDir,                  // 32/workspace
  virtualMode: true,                      // workspace 外へのアクセス制限
  inheritEnv: true,
});
  • virtualMode: true で workspace ディレクトリ外へのファイルアクセスを禁止
  • inheritEnv: true でホストの環境変数(ANTHROPIC_API_KEY など)を継承
  • README で警告される通り、ホスト上で直接 shell を実行するので、virtualMode の漏れに依存しないなら Docker 推奨
CopilotRuntime + LangChainAgentAdapter — フロントエンド ↔ エージェント Hub
// app/api/copilotkit/route.ts
const agent = createSlideAgent();

const runtime = new CopilotRuntime({
  agents: {
    default: new LangChainAgentAdapter({
      agent,
      stateKeys: ["files"],               // ★ React 側と同期する state キー
    }),
  },
});

export const { handleRequest: POST } = copilotRuntimeNextJSAppRouterEndpoint({
  runtime,
  endpoint: "/api/copilotkit",
});
  • CopilotRuntime: フロントエンドのチャット UI から POST を受けて、内部で agent を呼び出すサーバランタイム
  • LangChainAgentAdapter: langchain-copilotkit(連載著者の自作)が提供するアダプタ。LangGraph の streamEvents を CopilotKit のフォーマットに変換
  • stateKeys: ["files"]: React の useCoAgent 経由で エージェント state の files フィールドだけブラウザに同期できる
  • copilotRuntimeNextJSAppRouterEndpoint: Next.js App Router 用に handleRequest 関数を生成
useDefaultTool + CatchAllActionRenderProps — 任意ツール実行を UI で表示
// tool-call-renderer.tsx
export function ToolCallRenderer() {
  useDefaultTool(
    {
      render: (props: CatchAllActionRenderProps) => {
        const { status, name, args } = props;

        if (name === "generate_pptx") {
          return <GeneratePptxHandler result={props.result} status={status} />;
        }

        if (status === "inProgress") { /* spinner UI */ }
        if (status === "executing") { /* spinner with args UI */ }
        // status === "complete" の汎用 UI
      },
    },
    [],
  );
  return null;
}
  • useDefaultTool: 全ツール呼び出しに対する catch-all renderer
  • status: "inProgress" (LLM が tool_call を出力中) / "executing" (実行中) / "complete" (完了)
  • ツール名で分岐して特定の UI を出せる(generate_pptx は専用 handler)
PptxGenJS (ブラウザ) — クライアント側で .pptx 生成
async function generatePptxBase64(data: SlideData): Promise<string> {
  const pptx = new PptxGenJS();
  pptx.layout = "LAYOUT_WIDE";
  pptx.title = data.title;

  for (const s of data.slides) {
    const slide = pptx.addSlide();
    if (s.type === "title") {
      slide.background = { color: colors.primary };
      slide.addText(s.title, { x: 0.8, y: 1.5, fontSize: 36, color: "FFFFFF", bold: true });
    } else if (s.type === "section") { /* ... */ }
    else if (s.type === "content") { /* ... bullets */ }
  }

  return (await pptx.write({ outputType: "base64" })) as string;
}
  • ブラウザで動く PowerPoint 生成ライブラリ。サーバへの POST 不要 → ダウンロード遅延ゼロ
  • スライドタイプ別(title / section / content)にレイアウトを切替
  • base64 → Blob → URL.createObjectURL<a download> クリックでダウンロード
useSlideData (React Context) — Tool 結果と UI Preview の状態共有
// slide-context.tsx (推測される構造)
const { setSlideData, setPptxBase64 } = useSlideData();
// ...
setSlideData(data);
generatePptxBase64(data).then(setPptxBase64);
  • React Context で「スライドデータ」「PPTX base64」を全 component で共有
  • tool-call-renderer で生成 → slide-preview で表示 → ダウンロードボタンが両 component で同期

ファイル別の役割

ファイル 役割
agent/agent.ts createDeepAgent で Claude Sonnet 4.6 + LocalShellBackend + skill 1 個のエージェント定義
agent/generate-pptx-tool.ts Zod スキーマで JSON 検証する 検証ツール(実際の PPTX 生成はクライアント側)
agent/system-prompt.ts 「JavaScript 生成禁止 / npm install 禁止 / curl のみ」の制約。スキルに処理を寄せる方針
app/api/copilotkit/route.ts CopilotRuntime + LangChainAgentAdapter で エージェントを Next.js Route 化
app/page.tsx 2 カラム UI: 左 SlidePreview / 右 CopilotChat
app/components/slide-preview.tsx スライドのプレビュー描画
app/components/slide-context.tsx スライドデータ + PPTX base64 の React Context
app/components/tool-call-renderer.tsx 中核。tool 実行 UI 表示 + PptxGenJS で .pptx 生成 + ダウンロードボタン
workspace/AGENTS.md エージェントメモリ(簡潔な「タスク案内」と「対話の進め方」)
workspace/.agent/skills/pptx-generator/SKILL.md スキル定義。論文取得→ JSON保存→ 確認→ PPTX 生成の 3 ステップワークフロー

行レベルの工夫(中核ロジックの抜粋)

① エージェント組み立て (agent/agent.ts:10-30)
export function createSlideAgent() {
  const model = new ChatAnthropic({
    model: "claude-sonnet-4-6",                                              // ①
  });

  const backend = new LocalShellBackend({
    rootDir: workspaceDir,
    virtualMode: true,                                                       // ②
    inheritEnv: true,
  });

  return createDeepAgent({
    model,
    systemPrompt: SYSTEM_PROMPT,
    tools: [createGeneratePptxTool(backend)],                                // ③
    skills: ["./.agent/skills/"],                                            // ④
    memory: ["./AGENTS.md"],
    backend,
    checkpointer: new MemorySaver(),
  });
}
やってること なぜそうする
claude-sonnet-4-6 を明示 2026 年現在の最新 Sonnet。エージェントの推論力が直接結果品質に影響
virtualMode: true で workspace 外をブロック shell 実行のセキュリティ。curl 等の任意コマンド実行を許すので、ファイルシステム sandbox 必須
generate_pptx ツールに backend を渡す ツール内部で backend.readRaw(filePath) を呼ぶため。ツール ≠ backend ではなく、ツールは backend を使う設計
skills: ["./.agent/skills/"] でディレクトリ全部を読み込み pptx-generator/SKILL.md が自動でエージェントの prompt に組み込まれる
② Zod 検証付き JSON 読み込みツール (agent/generate-pptx-tool.ts:24-82)
export function createGeneratePptxTool(backend: BackendProtocol) {
  return tool(
    async (input) => {
      let raw: string;
      try {
        const fileData = await backend.readRaw(input.filePath);              // ①
        raw = fileData.content.join("\n");
      } catch {
        return JSON.stringify({
          success: false,
          error: "file_not_found",
          action: "ファイルパスを確認し、正しいパスで再度呼び出してください。",
        });
      }

      let json: unknown;
      try {
        json = JSON.parse(raw);
      } catch (e) {
        return JSON.stringify({
          success: false,
          error: "invalid_json",
          action: `${input.filePath} のJSON構文を修正し、再度このツールを呼び出してください。`,  // ②
        });
      }

      const result = slideDataSchema.safeParse(json);                        // ③
      if (!result.success) {
        const details = result.error.issues.map(...);
        return JSON.stringify({
          success: false,
          error: "validation",
          details,
          action: `${input.filePath} を以下のエラー内容に基づいて修正し、再度このツールを呼び出してください。`,
        });
      }

      return JSON.stringify({
        success: true,
        title: data.title,
        slides: data.slides,                                                 // ④
      });
    },
    {
      name: "generate_pptx",
      description: "スライドJSONファイルを読み込み、スキーマ検証して...",
      schema: inputSchema,
    },
  );
}
やってること なぜそうする
backend.readRaw(filePath)workspace/ 配下を読む virtualMode でも workspace 内なら自由に読める
エラー時に action: "修正して再度呼び出してください" を含める エラーメッセージそのものが LLM への指示。LLM はこの action を読んで自己修復 → 「failure-as-instruction」パターン
Zod safeParse でスキーマ検証 単純な JSON parse + 構造チェックを 1 行で。slideSchematype: "title"/"section"/"content" の discriminated union
成功時は slides 配列を返す(PPTX バイナリは返さない) バイナリ生成は client 側に委譲。サーバ側ツールは検証 + JSON pass-through のみという割り切り
③ Tool 結果を React で受けて PPTX 生成 (app/components/tool-call-renderer.tsx:169-207)
function GeneratePptxHandler({ result, status }: { result: unknown; status: string }) {
  const { setSlideData, setPptxBase64 } = useSlideData();
  const generatedRef = useRef(false);                                        // ①

  useEffect(() => {
    if (status !== "complete" || generatedRef.current) return;

    const parsed = typeof result === "string" ? JSON.parse(result) : result; // ②
    if (!parsed?.success) return;

    generatedRef.current = true;                                             // ③

    const data: SlideData = {
      title: (parsed.title as string) ?? "",
      author: parsed.author as string | undefined,
      slides: (parsed.slides as SlideData["slides"]) ?? [],
    };

    setSlideData(data);
    generatePptxBase64(data).then(setPptxBase64);                            // ④
  }, [status, result, setSlideData, setPptxBase64]);

  if (status === "complete") {
    return <DownloadCard />;
  }
  return <Spinner />;
}
やってること なぜそうする
useRef(false)生成済みフラグ React の StrictMode で useEffect が 2 回呼ばれても重複生成しないように
result が string なら JSON.parse LangChain ツールが文字列を返すか dict を返すか LLM 任せなのに対応
重複防止のためフラグを立てる 同じ tool call で何度も setPptxBase64 を走らせない
generatePptxBase64(data).then(setPptxBase64) で非同期生成→Context 更新 Context 更新で <DownloadCard />pptxBase64 を取得 → ダウンロードボタン表示
④ SKILL.md でエージェントのワークフローを Markdown で定義 (workspace/.agent/skills/pptx-generator/SKILL.md)
---
name: pptx-generator
description: arXiv論文URLからスライドを作成する。(1)論文取得・分析(内部処理)→(2)JSONで保存しダイジェスト提示→(3)確認後スライド生成。
---

# arXiv論文スライド生成スキル

## ワークフロー概要
1. 論文の取得と分析 → 内部処理。結果はユーザーに見せない
2. スライド構成をJSONファイルに保存し、ダイジェストをユーザーに提示する
3. ユーザー確認後、JSONを読み込みgenerate_pptxツールでスライド生成

## ステップ1: 論文の取得と分析
1. arXiv IDをURLから抽出する(例: 2301.00001)
2. `curl -sL https://arxiv.org/abs/{id}` でアブストラクトページを取得する
3. `curl -sL https://arxiv.org/html/{id}` でHTML本文を取得する
...
  • frontmatter の name / descriptiondeepagents がエージェントに「使えるスキル」として登録
  • ステップごとにcurl コマンド・JSON 書き出しパス・確認 UI の出し方まで具体的に書く
  • 「JavaScript を書かない」「npm install しない」等の制約は system-prompt と SKILL.md の両方で重複してかかる
  • Markdown だけでエージェントの振る舞いをカスタマイズできる → 非エンジニアもメンテ可能

学んだこと(要点)

  • TypeScript 版 LangChain エコシステムが Python に追いついたcreate_agent / Middleware は両言語で互換 API
  • deepagents の SKILL.md 構造は Claude Code と同型。スキルベース設計が業界標準になりつつある
  • CopilotKit + langchain-copilotkit が React フロントエンドとの接続を標準化。Streamlit のような「Python 単独で UI も書く」から、フロント/バックエンドが綺麗に分離したアーキテクチャに進化
  • ツールはサーバ、生成はクライアントの分業が賢い。重いライブラリ(PptxGenJS = 数 MB)を Node.js に持たず、ブラウザだけにロード
  • 「failure-as-instruction」パターン: ツールエラーにaction: "修正してから再度呼び出してください" を含めることで、LLM がそのまま指示として読み取って自己修復
  • virtualMode: true だけに安全を委ねない。README が明示する通り、本番では Docker での隔離が真の安全策
  • useDefaultTool + CatchAllActionRenderProps で全ツールに横断的 UI を当てられる → 開発体験が劇的に向上
  • React Context でツール結果 → UI 描画 → ダウンロードを 1 経路で繋ぐ。状態管理は Redux 等不要
  • langchain-copilotkit は連載著者自作の OSS。エージェント開発界隈の Hub になりつつある重要パッケージ
  • システムプロンプトを 14 行に絞り、残りはスキル ファイルに書く設計が新鮮。「prompt < skill」の概念

拡張アイデア

  1. SKILL を増やすpdf-analyzer スキル(arXiv 以外の PDF 論文を扱う)、citation-graph スキル(引用ネットワーク図を別 PPTX に)など
  2. stateKeys: ["files"] の活用 — React 側で useCoAgent で workspace のファイル一覧を取得 → サイドバーにツリー表示
  3. スライドプレビューの編集機能SlidePreview を編集可能にし、ユーザの直接編集が AGENTS.md に「ユーザの好み」として記録される循環ループ
  4. テーマ切替PptxGenJS のカラー定数を変数化し、UI から「ダーク」「ライト」「学会風」「企業風」を切り替え
  5. HTML / PDF / 画像など多形式出力generate_pptx の隣に generate_html / generate_pdf ツールを追加。同じ JSON から複数形式
  6. 本物の HITL 統合HumanInTheLoopMiddleware(第31回)を入れて、generate_pptx 実行前に承認ダイアログを出す
  7. Docker 化のリスク検証 — README で警告された LocalShellBackend のリスクを実証実験(workspace 外への意図しないアクセスを試みる)し、Docker 版との挙動差を確認

現代版に移植するなら

1. API キー設定は .env.op + op run に切り替える(CLAUDE.md ルール 8)
# 32/.env.op
ANTHROPIC_API_KEY=op://Personal/anthropic-api-key/credential
LANGCHAIN_API_KEY=op://Personal/langsmith-api-key/credential
LANGSMITH_PROJECT=sd-32
LANGCHAIN_TRACING_V2=true

起動:

op run --env-file=.env.op -- mise run dev
# または Docker
op run --env-file=.env.op -- mise run up

Docker 経由なら コンテナ内に環境変数注入される ので、Docker secrets 経由でも可。

2. システムプロンプトの強化

現状の system prompt は 14 行と短い。「JavaScript 禁止 / npm install 禁止」 だけでなく、「機密情報を含むファイルへのアクセス禁止」「workspace 外のディレクトリ走査禁止」等のセキュリティ宣言を追加すると LocalShellBackend のリスクが少し下がる。

3. LocalShellBackendDockerBackend か E2B に置き換え

deepagents の backend は差し替え可能。DockerBackend(公式提供)または E2BBackend(E2B クラウド sandbox 実行) に変えると、ホスト OS 隔離が真に効く。

4. tool-call-renderer.tsx の型整理

props.resultunknownJSON.parse してから parsed.success を見ているが、Zod スキーマで result を validate する一段を入れると堅い:

const ResultSchema = z.discriminatedUnion("success", [
  z.object({ success: z.literal(true), title: z.string(), slides: z.array(...) }),
  z.object({ success: z.literal(false), error: z.string(), action: z.string() }),
]);

const parsed = ResultSchema.parse(typeof result === "string" ? JSON.parse(result) : result);
5. PptxGenJS の dynamic import

tool-call-renderer.tsx 冒頭の import PptxGenJS from "pptxgenjs";初期バンドルに含まれるgenerate_pptx ツールを実際に使うまで PptxGenJS は要らないので:

const PptxGenJS = (await import("pptxgenjs")).default;

dynamic import → 初期ロード高速化

6. CSP 設定

Next.js の next.config.tsContent-Security-Policy ヘッダを設定。unsafe-eval 禁止script-src 'self' 等で XSS 攻撃面を絞る。


既知の不具合・注意点

  • SYSTEM_PROMPT の改行: agent/system-prompt.ts の文字列は \n 改行で良いが、long-token 圧迫の余地。SKILL.md 側にロジックを移しているので最小化されているのは良い
  • virtualMode: true の漏れ: README が警告する通り、完璧な sandbox ではない。信頼できない入力(任意ユーザの URL)を扱う production では Docker 必須
  • PptxGenJS の base64 サイズ制限: 大スライドだと base64 文字列が MB 級に。React context に持つので memory hog の可能性
  • MemorySaver の thread_id 自動生成なし: CopilotKit 側で thread_id を管理しているはず(要確認)。複数ユーザ並列時の隔離は要検証
  • useDefaultTool の deps が []: dependencies array が空なので、props を見ない参照になっている。React の hook ルール的に注意(実際の挙動は CopilotKit 内部に依存)
  • generated_pptx で例外時の UI が不完全: success=false のとき <DownloadCard /> も Spinner も出ない。エラーバナーを足すべき
  • ChatAnthropic のモデル ID ハードコード: claude-sonnet-4-6 を文字列直書き。環境変数で差し替えできるようにすべき
  • workspace/AGENTS.md が 簡素すぎる: 「タスク案内」と「対話の進め方」だけ。各論はスキル側にあるが、メモリ機能を活かすためにはユーザ好みなどをここに追記する設計が望ましい
  • HTML 版がない論文での挙動: SKILL.md は「PDF 限定の論文は対象外」と明記しているが、UI 側でエラーメッセージは綺麗に出るかは要確認
  • tool-call-renderer.tsx:213useDefaultTool 第 2 引数の []: React の useEffect 同様の deps だが、空配列のため一度しかフックされない。CopilotKit の意図通りかどうか docs 要確認

記事参照

  • Software Design 2026 年 5 月号(推定)連載第32回「deepagents + CopilotKit で arXiv スライド生成」
  • 関連: 第29回 STUDY_NOTES — LangChain 1.0 create_agent 入門。deepagents の Python 版相当
  • 関連: 第31回 STUDY_NOTES — Middleware 実戦。HITL を追加するなら必読
  • deepagents 公式: https://github.com/langchain-ai/deepagentsjs
  • CopilotKit 公式: https://docs.copilotkit.ai/
  • langchain-copilotkit(著者自作): https://github.com/mahm/langchain-copilotkit
  • PptxGenJS: https://gitbrent.github.io/PptxGenJS/

発展: claude -p(Claude Code headless)で組み直すなら

一言で: deepagents が「自前で組む部品」として提供していたもの(エージェントループ・skill・memory・仮想シェル sandbox・会話継続)が、claude -p ではほぼ全部ハーネス標準機能に化ける。残る自前実装は「claude -p の出力ストリームを React UI のプロトコル(AG-UI)に翻訳するアダプタ1枚」だけ。

主題が「自前のエージェント基盤を組む」から「既製ハーネスを Web UI の裏に挿す」へ移る。これが claude -p 版の学びの核。

何が「自前 → ハーネス標準」に化けるか(対応表)

32 回(deepagents が提供) claude -p 版(ハーネス標準) 自前実装の量
createDeepAgent({ model: ChatAnthropic }) =エージェントループ+LLM呼び出し claude -p 自体がエージェント。ループは書かない 消滅
LocalShellBackend(virtualMode, rootDir: workspace) =仮想シェル sandbox cwd=workspace--allowedTools ホワイトリスト+--permission-mode acceptEdits 消滅(フラグだけ)
skills: ["./.agent/skills/"](SKILL.md) .claude/skills/pptx-generator/SKILL.md同一フォーマット 移植のみ
memory: ["./AGENTS.md"] workspace/CLAUDE.md(cwd から自動ロード) 移植のみ
system-prompt.ts の SYSTEM_PROMPT --append-system-prompt "<方針>" 移植のみ
MemorySaver()(checkpointer・会話継続) --resume <session_id>(threadId 別に保持) 数行
generate_pptx ツール(Zod 検証・サーバ側) アダプタが確定ブロックを検証して generate_pptx を“合成” アダプタ内に内製
LangChainAgentAdapter(streamEvents → AG-UI) ClaudeCodeAgentAdapter(stream-json → AG-UI) ←今回の唯一の中核実装 1枚だけ自前
ブラウザ PptxGenJS で .pptx 生成 そのまま再利用(UI 無改変) 再利用

要点: CopilotKit から見れば LangChainAgentAdapter も ClaudeCodeAgentAdapter も「AG-UI の AbstractAgentで同じ穴にはまる。だから UI 側(CopilotChat / SlidePreview / tool-call-renderer / PptxGenJS)は一切触らずにエンジンだけ挿げ替えられる

データフロー

[Browser] CopilotChat(無改変)
   │ POST /api/copilotkit  (AG-UI: messages, threadId, runId)
[route.ts] CopilotRuntime → ClaudeCodeAgentAdapter.run(input)   ← 差し替えた seam
   │ 最新 user メッセージを prompt に、threadId 別 session を --resume に
   │ spawn: claude -p "<msg>"
   │          --output-format stream-json --include-partial-messages --verbose
   │          --permission-mode acceptEdits
   │          --allowedTools "Bash(curl:*),Read,Write,Edit,Task,Skill"
   │          --append-system-prompt "<方針>"  [--resume <sid>]
   │          (cwd = workspace/)
[claude -p] = エージェント本体
   skill: pptx-generator → curl で論文HTML → slides/{id}.json 保存
        → ダイジェスト提示 → 確認後 <<<PPTX_BEGIN>>>{確定JSON}<<<PPTX_END>>> を出力
   │ stdout: stream-json を 1 行ずつ
[ClaudeToAGUIMapper] stream-json → AG-UI イベントへ写像(純ロジック)
[Browser] CopilotChat にストリーミング表示
   generate_pptx の result(slide JSON) → tool-call-renderer が PptxGenJS で .pptx

中核: stream-json → AG-UI の写像(ClaudeToAGUIMapper

AbstractAgentrun(input): () => Observable<BaseEvent> を実装すればよい(AG-UI 公式の最小例)。claude -p の各行を次のように写す:

claude stream-json → AG-UI イベント
systemsession_id session_id を捕捉(次ターン --resume
assistanttext TEXT_MESSAGE_START / CONTENT / END
assistanttool_use(Bash curl 等) TOOL_CALL_START / ARGS / END(実ツールはそのまま素通し)
usertool_result TOOL_CALL_RESULT
text 中の <<<PPTX_BEGIN>>>…<<<PPTX_END>>> 検証して generate_pptx を合成(START/ARGS/END/RESULT)。番兵JSONは本文表示から除く
result RUN_FINISHEDcomplete()

generate_pptx は「アダプタ合成」にした(MCP を足さない判断)

32 の generate_pptx実は処理をするツールではなく、Zod 検証して slide JSON を返すだけの“UI へのハンドシェイク信号”(.pptx 本体生成はブラウザの PptxGenJS)。フロントの tool-call-renderer は name==="generate_pptx" のツール呼び出しと result に依存しているので、AG-UI ストリームにその形のイベントを誰かが流せばよい

選択肢は2つ:

  1. MCP ツール化 — claude に実ツール generate_pptx を MCP で渡す。利点は failure-as-instruction が native(検証エラーが同一ターンで返り claude が自己修復)。代償は MCP サーバ+--mcp-config+ツール名正規化(mcp__pptx__generate_pptxgenerate_pptx)で部品が増える。
  2. アダプタ合成(採用) — claude にツールを持たせず、skill が確定を番兵JSONで signal → アダプタが検証して generate_pptx イベントを合成。検証が Next.js サーバ側に来るので 32 の「サーバで検証・ブラウザで生成」という分業に忠実で、MCP サーバが要らない。

「claude -p をエンジンにする」が主題のアプリに2つ目の概念(MCP)を積むのは重い、という理由で アダプタ合成を採用。自己修復は「claude が skill 内スキーマで自己検証」+「検証エラーを success:false + action で UI に返し再出力させる」で代替(失敗時に LLM が読み取れる action 文字列を必ず載せる=failure-as-instruction の精神は踏襲)。

落とし穴・気づき

  • stateKeys: ["files"] は元々デッド: 32 のフロント(page.tsx / tool-call-renderer / slide-context)は useCopilotChat / CopilotChat / useDefaultTool だけを使い、useCoAgent も state files も参照していない。よって claude -p 版では STATE_SNAPSHOT 同期をゼロにしてよい(アダプタはテキストとツール呼び出しだけ流せば成立)。
  • bypassPermissions は使わない: headless でも無プロンプト化は --permission-mode acceptEdits--allowedTools ホワイトリストで足りる。allow も deny も無視する bypass は安全網が消えるので避ける。
  • sandbox の注記は 32 から継承: --allowedTools は LocalShellBackend(virtualMode) 同様“完全な隔離ではない”。信頼できない入力(任意 URL)を本番で扱うなら Docker、が 32 の README と同じ結論。
  • 認証・クォータの差: 32 は ANTHROPIC_API_KEY を ChatAnthropic が直叩き。claude -p 版は Claude Code の認証(サブスク or APIキー)を継承する。.env.op + op run で API キー注入してもよい。
  • 会話継続の鍵は threadId↔session_id: CopilotKit の threadId を claude の session_id に対応づけ、次ターンを --resume で同じ会話に継続させる(単一サーバインスタンス前提の Map で十分)。

このフォルダで実際に変えたもの(学習グレード・in-place)

ファイル 種別 内容
agent/claude-stream-mapping.ts 新規 写像の純ロジック(依存ゼロ・テスト対象)。ClaudeToAGUIMappermapClaudeStreamToAGUI
agent/claude-stream-mapping.test.ts 新規 node --test の契約テスト(5ケース・オフライン・決定的)
agent/claude-code-adapter.ts 新規 extends AbstractAgent。claude -p を spawn し写像を observer に流す配線(スケッチ)
app/api/copilotkit/route.ts 変更 LangChainAgentAdapterClaudeCodeAgentAdapter(エンジン交換の seam)
agent/index.ts 変更 新アダプタを export(旧 createSlideAgent はコメントで残置)
workspace/.claude/skills/pptx-generator/SKILL.md 新規 claude -p ネイティブ位置へ skill 移植(Step3 を番兵出力に変更)
workspace/CLAUDE.md 新規 AGENTS.md 相当の memory を cwd 直下へ

検証済み: agent/claude-stream-mapping.test.tsnode --test agent/claude-stream-mapping.test.ts で実行 → 5/5 pass(stream-json→AG-UI 写像、実ツール素通し、generate_pptx 合成、JSON 構文エラー、スキーマ違反)。

未実装(学習スケッチのため): アプリとして実際に動かすには bun add @ag-ui/client rxjs(+不要になる deepagents / @langchain/ / langchain-copilotkit を整理)と claude CLI が要る。旧 deepagents 版ファイル(agent/agent.ts / system-prompt.ts / generate-pptx-tool.ts / .agent/skills/ / AGENTS.md)は before 参照として残置*。本番化するときに除去+ package.json の依存差し替えを行う。--include-partial-messages を使うとトークン単位ストリーミング(stream_event の部分デルタ)も拾えるが、本スケッチはメッセージ単位の粒度で写像している。


作成: 2026-05-25 / 最終更新: 2026-06-30