
2026/07/06 23:50
Show HN: Agent Draw:あなたが話す間、エージェントが描画する TLDraw を基盤としたツール
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
最も大きな進歩は、「2draw」と呼ばれるインタラクティブな描画ゲームであり、React の tldraw SDK を基盤とし、AI エージェントに無限キャンバス上を自律的に描画する能力を付与します。Claude Opus 4.8 という高度なモデルとの統合により、このシステムは人間の介入なしに複雑な視覚的な指示(例:クリケットの場面の描写など)を成功裏に実行できます。これに対し、Claude Haiku や Google Gemini といった小規模なモデルは、単純な静的構成しか生成できず、あるいは正確な図形に関するタスクにおいては完全に失敗することがしばしば見られました。
技術的には、このプロジェクトは遅い単一ターンのリクエストを効率的なエージェントループに置き換え、冗長なビューアクションを削除することでパフォーマンスを最適化しています。また、Cloudflare Workers をホスティング用に採用し、複数の描画キャプチャがオーバーラップエラーを伴わずに順次処理されるように、特定の FIFO キュー機構を用いています。これにより、以前の遅いストリーミングインタフェースで発生していた問題を解消しています。システムでは、Mistral の Voxtral モデルを使用した音声文字起こしパイプラインを組み込み、ユーザーの発話をキャプチャするとともに、カスタムプロンプトを用いてモデルに指示を与え、適切な視覚的な形態を選択させる一方で、verbatim な文字起こしや停滞を引き起こすことを禁止しています。
開発者の観点からは、MIT ライセンス下にあるソースコードは、同様の生成アートアプリケーションを構築するための堅牢なテンプレートを提供します。ユーザーは、tldraw SDK の非商用利用向けの無料ホビーライセンスに従う条件の下で、抽象的なテキストプロンプトと精密なデジタルイラストの間のギャップを埋える高品質な視覚的創造能力を利用できるようになりました。
本文
tldraw 上に AI エージェントを統合:『Agent Draw』の開発と実装
React を対象とする無限キャンバス SDKtldraw上で、共有キャンバス形式の推測ゲーム「2draw」を開発した際のエージェント活用から着想を得た新たなツール、Agent Drawについて紹介します。このツールの主な機能は、ユーザーのプレゼンテーション中に、AI エージェントが代わりにキャンバス上に図を描画することです。
以下の内容は、化学授業でのデモ発表時のアシスタント活用事例および技術的な実装詳細に基づいています。
🚀 Agent Draw を今すぐ試してみよう(またはソースコードを入手しよう)
- ライブデモ:
tldraw-agent-draw-demo.james-664.workers.dev - GitHub リポジトリ:
github.com/ritza-co/tldraw-agent-draw-demo
💡 機能概要:あなたがプレゼン中に描画してくれるエージェント
ユーザーがキャンバス上に四角をドラッグして指示を入力すると、すぐに図が表示されます。これは AI エージェントがユーザーの発言に合わせてリアルタイムで描画を行っているものです。
- 待機列(キュー)による連続動作
- 複数の矩形を連続してドラッグすると、それらは待機列に登録されます。
- 指示された順番に描画処理が行われます。
- このシステムはすべて、無限キャンバスツールである tldraw の上で実装されています。
🛠️ 技術基盤と能力の検証
tldraw とは何か?
- React を対象とする無限キャンバス SDKです。
- ユーザーのマウス操作(形状作成・移動・矢印引くなど)と同じ API で、エージェントがコードを通じてこれらの操作を駆動します。
- tldraw は既に公式に「エージェント起動キット」を提供しており、その上に Cloudflare Worker のサポートがあります。Agent Drawはこのキットをベースに構築されています。
描画能力の検証結果
大規模言語モデル(LLM)が単純な基本要素から成る構成を適切に処理することが確認されました。
-
高性能モデル(例:
)claude-opus-4.8- 意思決定図やクリケットの風景スケッチなど、複雑な描画リクエストを完璧に処理します。
- ペンツールを使用した完全なスケッチも可能であり、高い野望(ambition)を持っています。
-
小型モデルの傾向:安易な妥協
- 高性能モデルと違い、野望が低下する傾向があります。
の例:claude-haiku-4.5- 意思決定図は作成可能ですが、クリケット描画ではペンツールを使わず、基本要素と静的なラベル付けに留まります。
-
より弱体化したモデル(例:
)google/gemini-2.5-flash-lite- タスクを早期に放棄する傾向が見られました。
⚙️ 開発実装の詳細
この機能は、以下のコンポーネントにより構成されています:
- 新しいキャンバスツール(ドラッグ操作)
- 音声処理パイプライン
- シリアライズされた描画キュー
- プロンプトセクション
1. ドラッグした領域をキャプチャする
tldraw のツールは状態機械(State Machine)です。
StateNode を継承し、以下の 3 つの状態を経由します:
idle → pointing → dragging
実際の主要処理はドラッグが完了する (
pointer-up) イベント時に実行されます。
class AreaCaptureDragging extends StateNode { static override id = 'dragging' private bounds: BoxModel | undefined = undefined override onPointerUp() { this.editor.updateInstanceState({ brush: null }) if (!this.bounds) throw new Error('Bounds not set') // キャプチャされた矩形(ページ座標系)をキャプチャセッションに渡す。 startCaptureSession(this.bounds) this.parent.transition('idle') } updateBounds() { if (!this.initialPagePoint) return const currentPagePoint = this.editor.inputs.getCurrentPagePoint() // ドラッグ中の境界計算 const x = Math.min(this.initialPagePoint.x, currentPagePoint.x) const y = Math.min(this.initialPagePoint.y, currentPagePoint.y) const w = Math.abs(currentPagePoint.x - this.initialPagePoint.x) const h = Math.abs(currentPagePoint.y - this.initialPagePoint.y) // tldraw 自身の選択ツールと同じインスタンス状態を利用して、ライブの選択ブラシ矩形を取得。 this.editor.updateInstanceState({ brush: { x, y, w, h } }) this.bounds = { x, y, w, h } } }
- 技術的なポイント:
を利用することで、tldraw 自体の選択ツールが使用する同じ状態を再利用し、ライブな選択ブラシ矩形を無料で取得できます。editor.updateInstanceState({ brush }) - 座標系: 境界(bounds)はページ座標系にあるため、ユーザーによる画面移動(パン)やズームに対しても正しく保たれます。
2. 話しながら聴き続ける
キャプチャ開始直後にマイクが起動します。
AreaRecorder はブラウザの MediaRecorder をラップした単純な実装です。
export class AreaRecorder { async start(): Promise<void> { this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(this.stream, { mimeType: this.mimeType }) this.chunks = [] recorder.ondataavailable = (event) => { if (event.data.size > 0) this.chunks.push(event.data) } recorder.start() this.recorder = recorder } async stop(): Promise<Blob> { // ……レコーダーを停止し、マイクを解放し、記録されたクリップを解決して返す…… return new Blob(this.chunks, { type: this.mimeType || 'audio/webm' }) } }
3. 音声からテキストへの変換
音声データ(Blob)は、Cloudflare Worker の
/transcribe ルートに送信されます。
- 実装: Mistral の Voxtral 音声認識モデルを使用。
- セキュリティ: サーバー側キーがデフォルトですが、訪問者が独自のヘッダーで API キーを指定した場合は独自キーが優先されます。
export async function transcribe(request: IRequest, env: Environment) { const form = await request.formData() const file = form.get('file') // 訪問者が独自に追加した場合、そのキーがサーバーのキーよりも優先される。 const mistralKey = request.headers.get('x-mistral-api-key') || env.MISTRAL_API_KEY const outForm = new FormData() outForm.append('file', file) outForm.append('model', 'voxtral-mini-transcribe-2507') const mistralResponse = await fetch('https://api.mistral.ai/v1/audio/transcriptions', { method: 'POST', headers: { Authorization: `Bearer ${mistralKey}` }, body: outForm, }) const data = (await mistralResponse.json()) as { text?: string } return new Response(JSON.stringify({ text: data.text ?? '' }), { headers: { 'Content-Type': 'application/json' }, }) }
4. 同時に複数の描画を処理する(キュー処理)
これは複数回のキャプチャを可能にする重要な部分です。
- 1 つ目の矩形が描画中にも、2 つ目以降の矩形をドラッグできます。
- キュー順に処理され、キャンバス上で競合しません。
- 設計: tldraw の atom 上のモジュールレベルの状態機械として実装(単一のレコーダー・FIFO キュー・単一コンシューマー)。
export function startCaptureSession(bounds: BoxModel): string { // 新しい描画を開始すると、まだ記録中の音声セッションを終了する。 if (recordingId) finalizeRecording(recordingId) const id = nextId() sessions.set([...sessions.get(), { id, bounds, status: 'recording' }]) const rec = new AreaRecorder() recorder = rec recordingId = id rec.start().catch(/* セッション上でマイクの許可エラーを表面化させる */) return id }
処理フロー:
- 新しいキャプチャ開始 → 現在記録中の音声セッション終了 → キュー登録。
- コンシューマーがキュー内のセッションを 1 つずつ処理(トランスクリプション → 描画)。
- シリアライゼーション: エージェントは一度に 1 つのリクエストしか実行しないため、新しいキャプチャは関係なく順番待ちになります。
async function processQueue(): Promise<void> { if (processing) return // 単一のコンシューマー processing = true try { while (queue.length > 0) { const id = queue.shift() as string const session = getSession(id) const blob = pendingBlobs.get(id) patchSession(id, { status: 'transcribing' }) const text = await transcribe(blob) patchSession(id, { status: 'drawing' }) await requestDrawInArea(agentRef, text, session.bounds) removeSession(id) // このキャプチャ用のオーバーレイをクリアする } } finally { processing = false } }
- 採用の背景: ライブストリーミング描画ではラグが発生したため、「一度にキャプチャしてその後話す」モデルに変更。また、キューによる導入で「2 つのキャプチャが互いにキャンセルする」という重なり問題を解決しました。
5. エージェントがタスク完了まで描画を続けるようにする
各セッションのトランスクリプトと境界情報を
requestDrawInArea に渡します。
- 重要: スターターキットのフルなエンティツループ (
) を使用し、単発のagent.prompt
は使いません。agent.request - 理由:
を使用すると、モデルは自らのターンを続け、追加要素がなくなるまで自動的に進み、1 つのコールですべての描画を完了できます。prompt - 対照的に、単発リクエストでは 1 つだけの形状しか描画されません。
export async function requestDrawInArea( agent: TldrawAgent, text: string, bounds: BoxModel ): Promise<number> { const area = { type: 'area' as const, bounds, source: 'user' as const } ensureMode(agent, 'working') const before = agent.editor.getCurrentPageShapeIds().size try { await agent.prompt({ message: buildAreaMessage(text), contextItems: [area] }) return agent.editor.getCurrentPageShapeIds().size - before } finally { ensureMode(agent, 'idling') } }
- 過去の改修: 以前は単発リクエスト問題から手動でループを実装しましたが、
の独自完走が確認されたため、足場コードを削除しました。agent.prompt
6. 動作を高速化する
固定領域での描画において、以下の 2 つのアクションを削除することで、1 キャプチャあたりのモデルコール数を約半減させました。
: カメラビュー移動。チャットパネルでは有用だが、エリアキャプチャではユーザーは「何が描かれたか」のみが見たいため不要(ラウンドトリップの原因)。setMyView
: リンターによる形状検証と修正スケジュール。これも最終的なモデルコールの増加要因であり、固定領域描画には寄与しないため削除。review
// review と setMyView を削除 — これらは描画ごとに追加のラウンドトリップを強制的に発生させ、 // 固定領域の結果改善には寄与しないため。Utils は他のモードでも引き続き登録される。 actions: [ ThinkActionUtil.type, CreateActionUtil.type, PenActionUtil.type, // ……残りのアクション…… ]
7. モデルに「描画」をさせ、「文字起こし」だけをさせない
システムプロンプトに以下の指示を追加しています。
- 視覚形式の自己評価: モデルに対し、どのような視覚形式が最適かを自ら評価させます。
- トランスクリプトの流用禁止: テキストをそのまま壁のように描画させません。
- 具体的な指示事項:
- 形状名(「赤い円を描く」)や図解は、複数ボックスではなくよく描画されたものを表示。
- 定義や説明は、全文テキストブロックではなく、キーワード付きのラベル付き図式。
- 図表やプロセスは、ラベル付きノードと矢印を使用。
- チャート:定量的または比較的内容の場合に使用。
- 動作方針:
- 曖昧な場合でも最善の見解を持つ。
- 決して停止しない(少なくとも 1 つの形状を必ず出力)。
- 単一のターンですべての結果を構築する推奨。
8. ツールをツールバーに登録
標準オーバーライド API を通じてツールを登録し、ビルトインツールの横にアイコンとショートカットが並ぶようにします。
const tools = [AreaCaptureTool, TargetShapeTool, TargetAreaTool] const overrides: TLUiOverrides = { tools: (editor, tools) => ({ ...tools, 'area-capture': { id: 'area-capture', label: 'Agent draw', kbd: 'a', icon: agentDrawIcon, onSelect() { editor.setCurrentTool('area-capture') }, }, }), } <Tldraw licenseKey={import.meta.env.VITE_TLDRAW_LICENSE_KEY} tools={tools} overrides={overrides} components={components} />
📜 ライセンスと利用について
- コードライセンス: MIT ライセンス。
- ローカル環境での利用は無料です。
- 下敷きとなっているスターターキットも同様に公開されています。
- tldraw SDK ライセンス: 別件専用のライセンスが必要です。
- 公衆向けデプロイにはライセンスキーの取得が必須。
- 非商業プロジェクトについては、tldraw から無料の愛好家ライセンスを受けることができます。