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.mdMarkdown ファイル。各スキルは「frontmatter(name, description)+ 本文(手順、ガイドライン)」で構成 - Memory =
AGENTS.mdMarkdown ファイル。長期記憶として全タスクで参照される - 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 rendererstatus:"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 行で。slideSchema は type: "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/descriptionでdeepagentsがエージェントに「使えるスキル」として登録 - ステップごとに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」の概念
拡張アイデア¶
- SKILL を増やす —
pdf-analyzerスキル(arXiv 以外の PDF 論文を扱う)、citation-graphスキル(引用ネットワーク図を別 PPTX に)など stateKeys: ["files"]の活用 — React 側でuseCoAgentで workspace のファイル一覧を取得 → サイドバーにツリー表示- スライドプレビューの編集機能 —
SlidePreviewを編集可能にし、ユーザの直接編集が AGENTS.md に「ユーザの好み」として記録される循環ループ - テーマ切替 —
PptxGenJSのカラー定数を変数化し、UI から「ダーク」「ライト」「学会風」「企業風」を切り替え - HTML / PDF / 画像など多形式出力 —
generate_pptxの隣にgenerate_html/generate_pdfツールを追加。同じ JSON から複数形式 - 本物の HITL 統合 —
HumanInTheLoopMiddleware(第31回)を入れて、generate_pptx実行前に承認ダイアログを出す - 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
起動:
Docker 経由なら コンテナ内に環境変数注入される ので、Docker secrets 経由でも可。
2. システムプロンプトの強化¶
現状の system prompt は 14 行と短い。「JavaScript 禁止 / npm install 禁止」 だけでなく、「機密情報を含むファイルへのアクセス禁止」「workspace 外のディレクトリ走査禁止」等のセキュリティ宣言を追加すると LocalShellBackend のリスクが少し下がる。
3. LocalShellBackend を DockerBackend か E2B に置き換え¶
deepagents の backend は差し替え可能。DockerBackend(公式提供)または E2BBackend(E2B クラウド sandbox 実行) に変えると、ホスト OS 隔離が真に効く。
4. tool-call-renderer.tsx の型整理¶
props.result が unknown で JSON.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 は要らないので:
で dynamic import → 初期ロード高速化。
6. CSP 設定¶
Next.js の next.config.ts に Content-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:213のuseDefaultTool第 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)¶
AbstractAgent は run(input): () => Observable<BaseEvent> を実装すればよい(AG-UI 公式の最小例)。claude -p の各行を次のように写す:
| claude stream-json | → AG-UI イベント |
|---|---|
system(session_id) |
session_id を捕捉(次ターン --resume) |
assistant の text |
TEXT_MESSAGE_START / CONTENT / END |
assistant の tool_use(Bash curl 等) |
TOOL_CALL_START / ARGS / END(実ツールはそのまま素通し) |
user の tool_result |
TOOL_CALL_RESULT |
text 中の <<<PPTX_BEGIN>>>…<<<PPTX_END>>> |
検証して generate_pptx を合成(START/ARGS/END/RESULT)。番兵JSONは本文表示から除く |
result |
RUN_FINISHED → complete() |
generate_pptx は「アダプタ合成」にした(MCP を足さない判断)¶
32 の generate_pptx は実は処理をするツールではなく、Zod 検証して slide JSON を返すだけの“UI へのハンドシェイク信号”(.pptx 本体生成はブラウザの PptxGenJS)。フロントの tool-call-renderer は name==="generate_pptx" のツール呼び出しと result に依存しているので、AG-UI ストリームにその形のイベントを誰かが流せばよい。
選択肢は2つ:
- MCP ツール化 — claude に実ツール
generate_pptxを MCP で渡す。利点は failure-as-instruction が native(検証エラーが同一ターンで返り claude が自己修復)。代償は MCP サーバ+--mcp-config+ツール名正規化(mcp__pptx__generate_pptx→generate_pptx)で部品が増える。 - アダプタ合成(採用) — 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も statefilesも参照していない。よって 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 |
新規 | 写像の純ロジック(依存ゼロ・テスト対象)。ClaudeToAGUIMapper + mapClaudeStreamToAGUI |
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 |
変更 | LangChainAgentAdapter → ClaudeCodeAgentAdapter(エンジン交換の 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.ts を node --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