mcpField
G
glove

glove

github.com/porkytheblack開発1252 回インストール今日更新
リポジトリ

glove について

gloveは、エージェント駆動のAIアプリケーションを構築するオープンソースのTypeScriptフレームワークです。開発者がツールとレンダラーを定義し、フレームワークがエージェントループを担います。Glove FoundryやMCP、メモリー、作業環境向けのパッケージも含みます。

主な機能

  1. 01エージェントループとツール実行のランタイム
  2. 02React連携とNext.jsのストリーミングルート
  3. 03glove-mcpによるMCPサーバー接続
  4. 04メモリー、事実、エージェント連携のパッケージ
  5. 05データベース、REPL、永続的な作業環境

活用シーン

  1. 01会話型AIアプリケーションの構築
  2. 02GloveエージェントからMCPサーバーを呼び出す
  3. 03エージェントのメモリーと共有情報を整理
  4. 04ツール利用向けのREPLや隔離環境を用意

MCP 対応

glove-mcpはMCPサーバーをGloveエージェントに接続し、マウント、接続、ツールのブリッジ機能を提供します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/porkytheblack/glove --skill glove

インストール後、Claude Code で/gloveを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

api-reference.md
examples.md
workflows.md

詳細

Glove Framework — 開発ガイド

あなたは Glove フレームワークのエキスパートです。Glove のコードを記述、デバッグ、レビューする際に、この知識を活用してください。

Glove とは

Glove は、AI を活用したアプリケーションを構築するためのオープンソースの TypeScript フレームワークです。ユーザーは会話の中で実現したいことを説明し、AI が呼び出す機能(ツール)を決定します。開発者はツールとレンダラーを定義し、Glove がエージェントのループを処理します。

Glove Foundry は、これらのプリミティブ上に構築された、推奨されるアプリケーションフレームワークおよびエージェントランタイムです。ファイルルーティング型のエージェントシステム、永続インスタンス、アプリケーションとトランスミッション、プレイブック、スケジュール、作業環境、インスペクターについては、専用の glove-foundry スキルを使用してください。Glovebox は非推奨であり、レガシー互換性のためのインターフェースとしてのみ残されています。

リポジトリ: https://github.com/porkytheblack/glove ドキュメントサイト: https://glove.dterminal.net ライセンス: MIT(dterminal)

目標、事実、フォームの準備

動的な目標を追加する場合、再利用可能なエビデンスを記録する場合、または既存の事実からフォームを準備する場合は、workflows.md をお読みください。目標は glove-memory/goals に、共有エビデンスは glove-facts に配置されます。このリファレンスでは、公開インポート、ツールのマウント、ライフサイクルと復旧のルール、トレースを維持したまま提供された Glove エージェントを通じて準備する方法について説明します。そのエージェントを提供すると、準備が可能になります。

パッケージの概要

パッケージ目的インストール
glove-coreランタイムエンジン:エージェントループ、ツール実行、表示マネージャー、モデルアダプター、デフォルトのインメモリー StoreAdapter である MemoryStore(ブラウザーセーフ — ネイティブ依存関係なし)pnpm add glove-core
glove-sqlite非推奨です。 SqliteStore — SQLite ベースの永続ストアです。プロトタイピングには glove-core の MemoryStore を、プロダクションには独自の StoreAdapter を使用してください。pnpm add glove-sqlite
glove-reactReact フック(useGlove)、GloveClient、GloveProvider、defineTool、<Render>、MemoryStore、レンダラーを同じ場所に配置した ToolConfigpnpm add glove-react
glove-nextSSE ストリーミング用の、1 行で記述できる Next.js API ルートハンドラー(createChatHandler)pnpm add glove-next
glove-foundry推奨されるエージェントアプリケーションフレームワークおよびランタイムです。 Effect ネイティブのファイルルーティング、型付き定義、永続的なインスタンスと会話、動的なインストールとプレイブック、スケジュール、作業環境、マルチエージェント構成、関連付けられたインスペクションを提供します。pnpm add glove-foundry
glove-mcpMCP サーバーを Glove エージェントにブリッジします:mountMcp、connectMcp、bridgeMcpTool、McpAdapter、検出サブエージェント discovermcp。オプトインの OAuth ヘルパーは glove-mcp/oauth にあります。pnpm add glove-mcp
glove-memoryエンティティ、エピソード、リソース、コンテキストのメモリー、会話形式、動的な目標(glove-memory/goals)。ストレージは独自に用意してください。リファレンスアダプターはプロセス内でのみ動作します。pnpm add glove-memory
glove-facts共有スコープ付きエビデンス、修正、再利用可能なリンク。目標とフォーム用に、呼び出し元から提供される Glove エージェントを使った任意の準備処理にも対応します。pnpm add glove-facts
glove-imageエージェント型の画像生成です。 画像処理を単一の generate_image ツールではなく、ワークフローとして実行します。PromptEnhancer[] パイプラインで「インビトゥイーン」を通して各プロンプトを構築します(キャラクター/シーンを原文のまま拡張、スタイル、LLM による書き換え、続いてアダプターの機能に合わせて制限し、劣化のたびにトレースを記録する必須の fitToModel())。独自ライブラリに永続的なキャラクターとシーンを保存し、役割(identity/style/composition/content/mask)付きの参照画像、各派生アセットに付与される Recipe の系譜(regenerate で再実行できます)、任意の sharp による決定論的な assemble、オプトインのビジョンレビュー/説明、4 つのスコープでの ImageUsage コスト追跡にも対応します。mountImage(glove, { adapter, assets, library, ... })。ドラフト v0.1。pnpm add glove-image
glove-meshインボックスのプリミティブを基盤とするエージェント間通信:mountMesh、MeshAdapter(トランスポートは独自に用意)、MeshNetwork とリファレンス実装の InMemoryMeshAdapter。4 つのツール — glove_mesh_send_message、glove_mesh_broadcast、glove_mesh_list_agents、glove_mesh_acknowledge。認証機能はありません(利用側で対応してください)。pnpm add glove-mesh
glove-continuum-signal時間をまたぐエージェント連携用のサブプロセスベースのランタイム基盤です。2 つのモードがあります:トリガー型(コールド、起動のたびに生成)と並行型(ウォーム、長時間稼働するサブプロセスにインラインで通知)。agent() ビルダー、ContinuumRunner(検出+監督+IPC)、ContinuumAdapter(永続化機能は独自に用意。デフォルトは MemoryAdapter)、ContinuumSubscriber(ライフサイクル+転送される Glove イベント)を提供します。エージェント間の通信には glove-mesh を組み合わせます。基盤が監督対象のサブプロセスを提供し、mesh がメッセージングを提供します。pnpm add glove-continuum-signal
glovebox-core非推奨です。 従来の Glovebox 作成用 CLI およびビルド用 CLI です。既存のデプロイの維持にのみ使用してください。新しいエージェントランタイムには glove-foundry を使用してください。pnpm add glovebox-core
glovebox-kit非推奨です。 互換性維持のために残されている、従来の Glovebox コンテナー内ランタイムです。(推移的なレガシーパッケージ)
glovebox-client非推奨です。 既存のデプロイ向けに残されている、従来の Glovebox クライアント SDK です。pnpm add glovebox-client
glove-scratchpadツール利用向けのデータベースエミュレーターです。 エージェントの機能をリレーショナルデータベースとして公開し、数十個のツール定義を読み込む代わりに、単一の execute_sql ツールでクエリを実行します。リソースはテーブルになります(Zod の schema を指定した defineResource、または resourceFromTool)。CRUD 動詞は基盤のツールに対応し、WHERE は引数を下位に渡し、information_schema は検出に使われます。トランザクションでは外部への効果をステージングします。mountDatabase(glove, { db })。MCP サーバーは glove-scratchpad/mcp 経由でテーブルに変換できます。クエリエンジンは glove-sql(同梱)です。PGlite はオプションです。以下の REPL が利用する、共有の glove-scratchpad/fns ToolFn カタログ(defineFn、fnFromTool、fnsFromMcp、sampleResultShapes)も含まれます。pnpm add glove-scratchpad
glove-lispツール利用向けの REPL サーフェス(Clojure 風)です。永続的なサンドボックス型 Lisp REPL 内で機能を関数として公開し、単一の execute_lisp ツールから利用できます。同じセッションで 2 つのモードを利用できます:ResourceTable モード(registerAll)と関数モード(registerFns、glove-scratchpad/fns カタログ)。LispSession.create() + mountLisp(glove, { session })。ドラフト v0.1。pnpm add glove-lisp
glove-jsツール利用向けの REPL サーフェス(JavaScript)です。glove-scratchpad/fns カタログを基盤として、単一の execute_js ツールで操作する小規模なサンドボックス型 JS インタープリター内で、機能を非同期関数として利用できます。JsSession.create() + mountJs(glove, { session })。段階的な検出は REPL 内から、短縮名(search/servers/fns/describe)とネイティブツール名のエイリアス(search_functions/list_servers/list_functions/describe_function)の両方で呼び出せるため、ツール名でプロンプトされたモデルも適切な呼び出しを行えます。A/B テストの上位アーム(97%)に到達するよう強化されています — JS-EXPLORATION.md を参照してください。ドラフト v0.1。pnpm add glove-js
glove-pythonツール利用向けの REPL サーフェス(Python)です。同じカタログを基盤として、単一の execute_python ツールで操作するサンドボックス型 Python サブセット内で、機能を関数として利用できます。ツール呼び出しはキーワード引数を取ります(github.list_pull_requests(state="open"))。内包表記、f 文字列、スライス、def/lambda に対応し、dunder 属性はブロックされます。PySession.create() + mountPy(glove, { session })。PY-EXPLORATION.md を参照してください。ドラフト v0.1。pnpm add glove-python
glove-working-environmentツール利用向けの永続的な作業環境です。 呼び出しごとにステートレスな REPL である scratchpad に対し、こちらは状態を蓄積する場所です。サンドボックス型のインメモリー仮想ファイルシステム(/inbox、/scripts、/skills、/std、/tmp、/out、/.env)を、閉じた動詞サーフェス — write_file、edit_file、read_file、ls、grep、describe、rm、mv、cp、run_script、run_tests、checkpoint、undo、redo、history — の背後で提供します。ホストが vision モデルを接続すると view_image(path, prompt, page?) も利用できます。これは視覚的な欠陥(ページからはみ出した表、棒のないグラフ、重なったタイトル)を検出する唯一の動詞です。PDF/デッキを自動でラスター化します。page は 1 始まりなので、スライド 3 の確認は 1 回の呼び出しで済みます。すべての実行は名前付きの永続スクリプト(export default async function (args))を介して行われます。スクリプトは書き込み時に検証され、兄弟 .d.ts は JSDoc から導出されます(ls /scripts が機能のカタログです)。スクリプトがインポートできるのは注入された機能のみです:env:fs、env:std、env:assert、登録済みアダプター、および相互のスクリプトです。環境から暗黙的に利用できるネットワーク/ホストのファイルシステム/プロセスへのアクセスはありません。外部機能にはホストがマウントしたアダプターが必要です。スクリプトはワーカースレッドで実行されます(計算処理中のスクリプトを停止する手段は worker.terminate() のみです)。起動時に /skills が生成され、各モジュールの正確なインポート行を含む実例レシピが配置されます。実測で失敗の第 1 位は推測したインポートであり、/skills/imports.md がその解決策です。切り詰めとスピルオーバーによって大きなデータをファイルに保持します。ファイルごとの undo/redo、実行履歴、名前付きの checkpoint フォークを使って復旧できます。ホストとの接続口:mount/export/snapshot/fromSnapshot。createWorkingEnvironment({ stdlib, limits }) + mountWorkingEnvironment(glove, { env })(構造的で、glove-core への依存はありません)。コアは依存関係ゼロです。大容量のフォーマットライブラリはアダプターパッケージ(glove-env-*)に含まれます。機能を公開する 4 つの方法 — 形状に応じて選択してください(以下の Working Environment セクションを参照):I/O → defineAdapter、ステートフルなビルダー → defineBuilder/defineBuilders、純粋な計算 → definePureModule(同期)、ライブラリではなく機能(MCP サーバー、モデル、HTTP API)→ defineTools。ホスト側のオプションで動詞を有効化します:vision は view_image を追加し、onPresent は present(/out のファイルを配信)を追加し、readOnlyPaths はエージェントが読み取り専用で編集できないサブツリーを指定します。glove-working-environment/testing でテストできます:createAdapterTestEnv(adapter) は { env, fs, script(), runScript(), audit() } を返します。audit() は types と実際のバインディングがどちらかの方向に食い違っている場合、ビルドを失敗させます。ドラフト v0.1。pnpm add glove-working-environment
glove-env-fetchenv:fetch として HTTP リクエスト、および VFS のダウンロード/アップロードを提供します。ホスト側のドメイン/オリジンポリシー、ネイティブの DNS/IP 保護、キャンセル、認証情報エイリアスに対応します。HTTP ファイルと以下のスコープ付きシークレットを参照してください。pnpm add glove-env-fetch
glove-env-secretホスト側キーストアを env:secret として提供します:スコープ付きメタデータ/参照、オプトインによる開示/書き込み、VFS スナップショット外のプラガブルストレージ。pnpm add glove-env-secret
glove-env-documentsglove-working-environment 用の 標準ライブラリーアダプター:ドキュメント → env:documents。1 つのドキュメント仕様から PDF(pdf-lib)と DOCX(docx)の両方を生成します:{ heading }、{ text }、{ bullets }、{ table }、{ image }、{ pageBreak } — 折り返しとページ割りは自動です。describe(path) はバイト列から形式を判定します。pdf.merge/split/extractPages/setMetadata/stamp。pdf.extractText にはオプションの pdfjs-dist ピア依存関係が必要です(グリフから文字へのデコードはフォント/CMap の問題です。単純なスキャンではもっともらしいでたらめが返るため、代わりに処理を拒否します)。docx.extractText は追加の依存関係なしで node:zlib を使って OOXML zip を読み取ります。PDF では非 Latin-1 文字を音訳します(標準フォント)。完全な Unicode には DOCX を出力してください。ドラフト v0.1。pnpm add glove-env-documents
glove-env-spreadsheetsglove-working-environment 用の 標準ライブラリーアダプター:スプレッドシート → env:spreadsheets(exceljs)。describe(path) はシート/サイズ/ヘッダー/サンプル行 1 行を一定のコストで返します。read はヘッダー行をキーとするレコードをプレーンな JSON として返します。数式は結果に、リッチテキストは文字列に、日付は ISO 形式に、エラーセルは #DIV/0! にフラット化されます。さらにページング用の totalRows、offset、limit を返します。空のヘッダーは列文字になり、重複したヘッダーには _2 が付くため、列は削除されません。append はレコードのキー順ではなくシートのヘッダー順に従います。writeSheets、toCsv/fromCsv。.xlsx のみです。ドラフト v0.1。pnpm add glove-env-spreadsheets
glove-env-imagesglove-working-environment 用の 標準ライブラリーアダプター:画像 → env:images(sharp)。画像バイト列がコンテキストウィンドウに入ることはありません。describe(path) はピクセルをデコードせずに、形式/寸法/色空間/アルファ/EXIF の向きを報告します。stats(path) はチャンネルのばらつきと主要色を追加で返します(「このページは空白か?」への回答に使えます)。resize/convert/crop/rotate/composite/thumbnail/contactSheet に加え、キャプションや透かしを描画する text(input, output, {...}) を提供します。アニメーション GIF/WebP は resize/convert/crop/thumbnail/composite 後も保持されます。判断は出力形式を基準に行われるため、.gif/.webp ではすべてのフレームが保持され、それ以外では最初のフレームが使われます。rotate はアニメーションソースを拒否します(libvips ではフレームをそのまま回転できず、180°回転するとアニメーションが逆再生になるためです)。オプトアウトには { animated: false } を指定します。SVG は scale/density を使ってラスター化され、.svg の出力は拒否されます。{ format } で上書きしない限り、出力拡張子によってエンコーダーが選択されます。EXIF の向きは暗黙的に適用されず、明示されます。rotate(in, out) を角度なしで呼び出すと正規化されます。ドラフト v0.1。pnpm add glove-env-images
glove-env-slidesglove-working-environment 用の 標準ライブラリーアダプター:スライド → env:slides(pptxgenjs)。create(spec, output) はレイアウトとサイズ調整が自動の 1 回呼び出しルートです。デッキは入力としても扱えます:describe(path)、スライドごとのテキストを返す extract(path)、読みやすいアウトラインを返す outline(path) は、いずれも OOXML zip を解析します。完全な制御が必要な場合は、defineBuilder 経由で pptxgenjs 独自のビルダー API を変更せずに公開します。new Pptx()、addSlide()、addText()、チェーン呼び出し、引数内のノード参照はすべてワーカー境界を越えて動作します。.pptx のみです。ドラフト v0.1。pnpm add glove-env-slides
glove-env-zipglove-working-environment 用の 標準ライブラリーアダプター:アーカイブ → env:archives。zip、tar、tar.gz の圧縮/展開を、node:zlib とコンテナ形式自体以外の依存関係なしで行えます。describe(path)/list(path) は展開せずにエントリーとサイズを報告します。extract(path, dir, { include? }) と create(dir, output, { glob?, format? }) も提供します。展開前に環境のバイト予算と照合するため、収容できないアーカイブは処理前に拒否され、実行してから判明することはありません。ドラフト v0.1。pnpm add glove-env-zip
glove-env-mediaglove-working-environment 用の 標準ライブラリーアダプター:メディア → env:media(@ffmpeg-installer/@ffprobe-installer 経由で同梱される ffmpeg/ffprobe)。コンテキストにバイト列を入れずに音声と動画を扱います。describe(path) は ffprobe からコンテナー/ストリーム/再生時間/ビットレートを報告します。その後、thumbnail、frames(input, dir, { fps?, limit? })、clip、concat、transcode、extractAudio、slideshow を利用できます。さらに loudness(統合 LUFS/トゥルーピーク/レンジ)、normalize(2 パスの loudnorm、動画はコピー)、waveform、extractSubtitles(.srt/.vtt/.ass/.ssa。処理中に変換します)、burnSubtitles(埋め込みトラックまたはツリー内のファイルを使う libass)も提供します。extractAudio は codec/bitrate/sampleRate/channels を受け取り、describe は各ストリームの track/language/title を報告します。ドラフト v0.1。pnpm add glove-env-media
glove-env-renderglove-working-environment 用の 標準ライブラリーアダプター:レンダリング → env:render。ドキュメントをページ PNG にラスター化し、エージェントが生成物を目で確認できるようにします:render(input, outDir, { pages?, scale?, maxWidth? }) → { pages, format, totalPages }。PDF と画像に追加の依存関係はありません(@napi-rs/canvas 上の pdfjs-dist)。.docx/.xlsx にはヘッドレス LibreOffice が必要です。一方 .pptx では、独自の OOXML ジオメトリーから描画するレイアウト概略図にフォールバックします(実際のフレームとテキストを表示し、テーマ/フォント/グラフは表示しません。結果には approximate: true が付き、スライドからのはみ出し/重なり/空白の確認には適していますが、見た目の評価には適しません)。また、libreoffice-core だけでは不十分です。インポートフィルターが同梱されておらず、終了コード 0 を返しながらすべての変換に失敗します。アダプターはこれを検出して明示します。形式はマジックバイトを確認してから拡張子で決定するため、.pptx という名前の PDF が LibreOffice に渡されることはありません。長辺は 1600px に制限されます(ビジョンモデルはピクセル数に応じて料金が発生します)。view_image 動詞と組み合わせて使用します。ドラフト v0.1。pnpm add glove-env-render
glove-env-motionglove-working-environment 用の 標準ライブラリーアダプター:モーション → env:motion。エージェントが React コンポーネントを記述し、render(scene, out, opts) で .mp4/.webm/.gif/PNG フレームを生成します。still(scene, out, { frame }) では単一の PNG を生成するため、動画、タイトルカード、グラフ画像を同じ機能で作成できます。時間は計測せず、置き換えられます。requestAnimationFrame はレンダラーが処理するキューになり、performance.now()/Date.now() はレンダラーが設定する値を返すため、60 フレームのシーンを 2 回実行するとバイト単位で同一になります。シーンは useFrame()(注入された glove/motion ランタイム経由のフレームの純粋関数)または変更なしの React Native Reanimated です。どちらのシグナルも各フレームで進み、もう一方の種類ではそれぞれ動作しないため、モードを選ぶ必要はありません。React、Babel/worklets ツールチェーン、ffmpeg はパッケージに同梱されています。ホストに必要なのはブラウザーのみです(mac/Windows/Linux で Chrome/Edge/Chromium を自動検出します)。Reanimated にはオプションの react-native-reanimated + react-native-web ピア依存関係が必要です。limits: MOTION_LIMITS を渡してください — runTimeoutMs で処理を完了できない場合は、レンダリング開始前に拒否し、正確な修正方法を示します。doctor()/glove-motion-doctor バイナリー/capabilities()/生成される /std/motion/README.md は、各行の修正コマンドとともに「このホストでレンダリングできるか」を回答します。ドラフト v0.1。pnpm add glove-env-motion
アプリケーションの形態に応じて、利用するインターフェイスを選びます。 集中的な会話型ウェブインターフェイスには glove-react + glove-next、単一のヘッドレスエージェントには glove-core、ファイルルーティング、ランタイムインスタンス、インバウンド/アウトバウンド連携、自動化、マルチエージェント作業、インスペクションを備えた完全なエージェントアプリケーションには glove-foundry を使用します。Model Context Protocol 経由でサードパーティ製ツールを利用する場合は、MCP 連携を参照してください。
フレームワークの内容
  • glove-core — エージェントループ、ツール、表示スタック、ストア/モデル/サブスクライバーのアダプター、コンテキストの圧縮、インボックス、フック/スキル/サブエージェント、MemoryStore を既定のインメモリ StoreAdapter として提供します。
  • glove-react — defineTool、<Render>、useGlove、MemoryStore、createRemoteStore、createEndpointModel、createRemoteModel による、ツールと同じ場所に配置するレンダラーです。
  • glove-next — createChatHandler(1 行で作成できる SSE ルート)、音声トークンハンドラーです。
  • glove-foundry — ファイルベースのルーティング、型付け、可観測性を備えたエージェントシステム向けに推奨される、Effect ネイティブのアプリケーションフレームワークおよびランタイムです。専用の glove-foundry スキルを参照してください。
  • glove-sqlite — 非推奨です。永続化用の SqliteStore を提供します(サーバー側のみ)。
  • glove-voice — 全二重音声パイプライン:STT/TTS/VAD アダプター、GloveVoice、useGloveVoice、useGlovePTT、<VoicePTTButton>。
  • glove-voice-s2s — Glove エージェントをリアルタイム音声間モデル(OpenAI Realtime gpt-realtime、Gemini Live)で直接実行します:RealtimeAgent(サーバー側。エージェントのモデルスロットからアダプターを導出)、s2sDrivenModel(設定を保持するモデルスロット:プロバイダー/モデル/音声/ターンテイキングの各設定)、createS2SAdapter(引数 → S2S_* 環境変数へのフォールバック)、型付きの OpenAITurnDetection/GeminiRealtimeInputConfig、切り詰め同期を伴う無条件の割り込み(モデルは呼び出し側が実際に聞いた内容を学習します)。
  • glove-voice-avatar — S2S スタック上のレンダリングレイヤーとしてのライブアバター:AvatarAdapter コントラクト(connect → クライアントが接続する view、sendAudio/endUtterance/interrupt。準拠性を強制)と runAvatarConformance、TavusEchoAdapter(echo PAL、Daily ルーム、sendInteraction クーリエ。インタラクションはデータチャネルのみ)、AnamPassthroughAdapter(音声パススルーのペルソナ。ブラウザー SDK が音声入力を所有 → sendCommand クーリエ。Anam のプラン上限により、Growth ティア未満では数分ごとにセッションが強制終了されます — disconnect() + connect() で更新してください)、1 回の呼び出しで接続する attachAvatar(rt, avatar) ブリッジ。
  • glove-voice-livekit — アダプターとしての LiveKit:LiveKitTransport(ルームへの参加、エージェント音声トラックのペース配信、リモートマイク音声の PCM イベント化、JSON データチャネル、clear() = サーバー主導の割り込みフラッシュ)と attachRealtime(rt, transport)。LiveKit アバターの TavusLiveKitAvatar/AnamLiveKitAvatar は同じ AvatarAdapter コントラクトを実装します — プロバイダーのワーカーはあなたのルームに参加し(トークン種別 agent、lk.publish_on_behalf)、lk.audio_stream バイトストリームと lk.clear_buffer RPC 経由で制御されます。トークンは mintParticipantToken/mintAvatarToken で発行します。
  • glove-mcp — MCP サーバーを一級のツールとして扱います:mountMcp、connectMcp、bridgeMcpTool、McpAdapter(コンシューマーが会話ごとに提供する接続点)。discovermcp 検出サブエージェント(glove.defineSubAgent(discoverySubAgent({...})) で登録)。オプトイン方式の OAuth ヘルパーは glove-mcp/oauth にあります(runMcpOAuth、FsOAuthStore、MemoryOAuthStore、McpOAuthProvider)。
  • glove-memory — 5 つのサブシステム(エンティティグラフ/エピソードタイムライン/リソースファイルシステム/アンビエントコンテキスト/フォーム)を備えたメモリレイヤーと、それぞれに対応する useMemoryReader/useMemoryCurator、useEpisodicReader/useEpisodicCurator、useResourcesReader/useResourcesCurator、useContext、useFormRunner/useFormReader のヘルパーファミリーです。ストレージに依存しないアダプターコントラクトと、開発/テスト用の参照実装 InMemory* アダプターを提供します。
  • glove-image — エージェント型の画像生成です。mountImage(glove, { adapter, assets, library, pipeline?, model?, review?, usage?, onUsage?, curate?, candidates?, requirePermission? }) は、glove_image_generate/_edit/_regenerate/_import/_describe/_asset_list/_assemble/_usage と _character_*/_scene_* CRUD をまとめます。プロンプトパイプライン(expandCharacters、expandScenes、styleDirective、negativeDefaults、llmEnhance、終端の fitToModel)は、各ステージのトレースを記録しながら、意図を最終リクエストに変換します。参照アダプター:glove-image/openrouter の openrouterImages()。
  • glove-mesh — インボックスプリミティブを基盤とするエージェント間メッセージングです:mountMesh(glove, { adapter, identity }) はエージェントを登録し、glove_mesh_send_message/_broadcast/_list_agents/_acknowledge をまとめます。MeshAdapter はコンシューマーが提供するトランスポート(BYO)です。プロセス内の開発/テスト用に InMemoryMeshAdapter と MeshNetwork を同梱しています。各エージェントは独自のインボックスを保持します。受信メッセージは解決済みの InboxItem として格納され、既存のインボックス注入経路により次の ask() で表面化します。認証はなく、送信者 ID は検証されません。
  • glove-continuum-signal — station-signal をモデルに、エージェント向けに構成したサブプロセスベースのランタイム基盤です。agent("name").input(zod).triggered()|.concurrent() ビルダーでブランド付きエージェントを作成します。ContinuumRunner はディレクトリからエージェントを検出し、並行エージェントを事前に起動して、アダプターキューからトリガー実行をディスパッチし(スポーンごとに分離)、ウォーム状態のサブプロセスに notify IPC エンベロープをインラインでルーティングします。ContinuumAdapter は永続化コントラクトです(MemoryAdapter を同梱。SQLite/Postgres などはコンシューマーが用意します)。単一の太い onAgentEvent(envelope) サブスクライバーが、任意の子プロセスからのすべての Glove SubscriberEvent を、エージェント ID を付与して上流へ転送します。Mesh はファクトリー内でエージェントごとにマウントすることで連携します — 特別な IPC 機構は不要です。
  • glove-scratchpad — LLM のツール使用向けデータベースエミュレーターです。多数のツール定義を読み込む代わりに、1 つの execute_sql ツールでクエリできるリレーショナルデータベースとして機能を公開します。リソース(github_pr、emails、time、images など)はテーブルになり、CRUD 動詞は(異なる場合もある)基盤ツールに対応付けられます。Database.create()、defineResource({ name, schema, keys?, volatility, select?, insert?, update?, delete? })(Zod ファースト — スキーマはカラムであると同時に、エンドツーエンドの行型です)、または resourceFromTool(tool, { name, volatility, schema })、mountDatabase(glove, { db }) は execute_sql + explain_sql をまとめ、プロンプトを準備します。検出は information_schema 経由で行います。WHERE の等価条件は引数としてプッシュダウンされます(Steampipe の必須キー方式)。書き込みはプレビュー用に BEGIN … COMMIT 内でステージングされます。EXPLAIN はリゾルバーを呼び出さずに、クエリがどのツールにアクセスするかを報告します。すべてのステートメントは、ツールの実行前に解析され、セキュリティチェックを通過します。MCP サーバーは glove-scratchpad/mcp(mcpResources/mountMcpDatabase)経由でテーブルになります。バックエンドは glove-sql(同梱、依存関係なし)または PgliteBackend(glove-scratchpad/pglite)です。
  • glove-working-environment — 固定されたツール一覧の代わりに、モデルがコードを書いて操作する永続ファイルシステム + スクリプトランタイムです。createWorkingEnvironment({ stdlib, limits }) + mountWorkingEnvironment(glove, { env }) を使用します。スクリプトはワーカースレッドで実行され、ホストから注入されたものだけをインポートできます。ライブラリは、その形状に応じて選択する 4 つの経路のいずれかでスクリプトに提供されます — defineAdapter(I/O)、defineBuilder/defineBuilders(状態を持つビルダー API。変更なし)、definePureModule(同期的な純粋計算)、defineTools(ライブラリではなく機能として提供:MCP サーバー、Glove ツール、通常の非同期関数)。フォーマットアダプターは glove-env-* として別途提供されます。「Working Environment」セクションを参照してください。

アーキテクチャの概要

User message → Agent Loop → Model decides tool calls → Execute tools → Feed results back → Loop until done
                                                          ↓
                                                   Display Stack (pushAndWait / pushAndForget)
                                                          ↓
                                                   React renders UI slots
コアコンセプト
  • Agent — ルーター/ナビゲーションロジックに代わる AI コーディネーターです。ツールを読み取り、どれを呼び出すか判断します。
  • Tool — 機能を表します。名前、説明、inputSchema(Zod)、do 関数、任意の render と renderResult で構成されます。
  • Display Stack — ツールが UI スロットを追加するスタックです。pushAndWait はツールをブロックし、pushAndForget はブロックしません。
  • Display Strategy — スロットの表示ライフサイクルを制御します。"stay"、"hide-on-complete"、"hide-on-new" があります。
  • renderData — do() が返す、クライアント専用のデータです。AI モデルには送信されません。履歴のレンダリングに renderResult が使用します。
  • Adapter — Model、Store、DisplayManager、Subscriber 用のプラグ可能なインターフェースです。アプリのコードを変更せずにプロバイダーを切り替えられます。
  • Context Compaction — コンテキストウィンドウの制限内に収めるため、長い会話を自動で要約します。ストアにはメッセージ履歴全体が保持されるため(フロントエンドでチャット全体を表示できます)、Context.getMessages() は最後の要約で分割し、モデルには要約後のコンテキストのみを表示します。要約メッセージには is_compaction: true が設定されます。
  • Inbox — インスタンス間通信に使う、永続的な非同期メールボックスです。エージェントは、今すぐ解決できないリクエスト(テキスト)を投稿し、外部サービスが後から解決します(テキスト応答)。解決済みのアイテムは、次回の ask() 呼び出し時にエージェントのコンテキストへ自動的に挿入されます。アイテムはブロッキング(エージェントが待機する)にも、ノンブロッキングにもできます。ストアが inbox メソッドをサポートしている場合、組み込みの glove_post_to_inbox ツールが自動登録されます。
  • Extensions(hooks、skills、subagents) — /hookname は、ビルダーで定義されたハンドラーを実行し、エージェントを完全に制御できます(強制的なコンパクション、モデルの切り替え、ターンの短絡など)。/skillname は実際のユーザーメッセージの前に合成ユーザーメッセージを生成します(is_skill_injection: true が設定されます)。defineSubAgent({ name, factory }) はサブエージェントを登録します。メインエージェントは自動登録される glove_invoke_subagent ツールを介してサブエージェントにルーティングできます。ユーザーの @name テキストは glove によって解析されず、そのままモデルに渡され、ルーティングシグナルとして機能します(Claude Code のサブエージェント規約に準じます)。/ トークンは、再度解析されたときにプレースホルダーがトリガーされないよう、トリガー不能なプレースホルダー([invoked_extension__hook_<name>]/[invoked_extension__skill_<name>])に置き換えられます。これにより、拡張機能が実行されたことをモデルに知らせます。バインドされていない / トークンはそのまま残るため(/usr/local はそのまま保持されます)、スキルをエージェントに公開し(exposeToAgent: true)、自動登録される glove_invoke_skill ツールを介してエージェントが呼び出せるようにできます。
  • MCP catalogue + adapter — glove-mcp は 2 つの要素を導入します。アプリがサポートするサーバーを記述する静的な McpCatalogueEntry[] と、有効な ID を保持してアクセストークンを解決する会話単位の McpAdapter です。mountMcp は以前に有効化されたサーバーを再読み込みし、glove.defineSubAgent(discoverySubAgent({...})) を介して discovermcp ディスカバリーサブエージェントを登録します。モデルは glove_invoke_subagent({ name: "discovermcp", prompt: "..." }) を介してこれを呼び出し、会話中にサーバーを検索して有効化します。
  • Mesh network — glove-mesh を使うと、Glove エージェントは利用側が提供する MeshAdapter(トランスポート)を介して相互にメッセージを送信できます。各エージェントは独自の StoreAdapter と inbox を保持します。A が B に送信すると、フレームワークは status: "resolved" の InboxItem を B のストアに追加し、既存の inbox 挿入処理を通じて、B の次回の ask() 時に内容が表示されます。ブロッキング送信では blocking: true の保留アイテムが追加され、ack または in_reply_to の返信で解決されます。v1 では認証はありません。
  • Continuum(サブプロセスランタイム) — glove-continuum-signal は、Glove エージェントを Node サブプロセスとして監視します。Triggered エージェントはコールド状態で、起動のたびに新しいプロセスが生成されます(毎回、永続的な StoreAdapter から再開します)。Concurrent エージェントはウォーム状態で、インラインで通知されます。agent() ビルダーは .triggered()/.concurrent() の後にモード固有の型へ分岐するため、モード固有のセッター(.retries()、.every())は型レベルで制限されます。.store(name => StoreAdapter) で設定した永続ストアを使い、エージェント群全体に同じストア実装を注入できます。実行状態の唯一の情報源は親であり、子は IPC のみを送信します。Mesh の統合はファクトリーでマウントし、基盤レイヤーをエージェント間プロトコルに含めないようにします。

クイックスタート(Next.js)

1. インストール
pnpm add glove-core glove-react glove-next zod
2. サーバールート
// app/api/chat/route.ts
import { createChatHandler } from "glove-next";

export const POST = createChatHandler({
  provider: "anthropic",     // or "openai", "openrouter", "gemini", etc.
  model: "claude-sonnet-4-20250514",
});

.env.local に ANTHROPIC_API_KEY(または OPENAI_API_KEY など)を設定します。

3. defineTool でツールを定義する
// lib/glove.tsx
import { GloveClient, defineTool } from "glove-react";
import type { ToolConfig } from "glove-react";
import { z } from "zod";

const inputSchema = z.object({
  question: z.string().describe("The question to display"),
  options: z.array(z.object({
    label: z.string().describe("Display text"),
    value: z.string().describe("Value returned when selected"),
  })),
});

const askPreferenceTool = defineTool({
  name: "ask_preference",
  description: "Present options for the user to choose from.",
  inputSchema,
  displayPropsSchema: inputSchema,       // Zod schema for display props
  resolveSchema: z.string(),             // Zod schema for resolve value
  displayStrategy: "hide-on-complete",   // Hide slot after user responds
  async do(input, display) {
    const selected = await display.pushAndWait(input);  // typed!
    return {
      status: "success" as const,
      data: `User selected: ${selected}`,         // sent to AI
      renderData: { question: input.question, selected },  // client-only
    };
  },
  render({ props, resolve }) {           // typed props, typed resolve
    return (
      <div>
        <p>{props.question}</p>
        {props.options.map(opt => (
          <button key={opt.value} onClick={() => resolve(opt.value)}>
            {opt.label}
          </button>
        ))}
      </div>
    );
  },
  renderResult({ data }) {               // renders from history
    const { question, selected } = data as { question: string; selected: string };
    return <div><p>{question}</p><span>Selected: {selected}</span></div>;
  },
});

// Tools without display stay as raw ToolConfig
const getDateTool: ToolConfig = {
  name: "get_date",
  description: "Get today's date",
  inputSchema: z.object({}),
  async do() { return { status: "success", data: new Date().toLocaleDateString() }; },
};

export const gloveClient = new GloveClient({
  endpoint: "/api/chat",
  systemPrompt: "You are a helpful assistant.",
  tools: [askPreferenceTool, getDateTool],
  // getSessionId: () => fetch("/api/session").then(r => r.json()).then(d => d.id),
});
4. プロバイダーとレンダリング
// app/providers.tsx
"use client";
import { GloveProvider } from "glove-react";
import { gloveClient } from "@/lib/glove";

export function Providers({ children }: { children: React.ReactNode }) {
  return <GloveProvider client={gloveClient}>{children}</GloveProvider>;
}
// app/page.tsx — using <Render> component
"use client";
import { useGlove, Render } from "glove-react";

export default function Chat() {
  const glove = useGlove();

  return (
    <Render
      glove={glove}
      strategy="interleaved"
      renderMessage={({ entry }) => (
        <div><strong>{entry.kind === "user" ? "You" : "AI"}:</strong> {entry.text}</div>
      )}
      renderStreaming={({ text }) => <div style={{ opacity: 0.7 }}>{text}</div>}
    />
  );
}

または、完全に手動で制御するには useGlove() を直接使用します。

// app/page.tsx — manual rendering
"use client";
import { useState } from "react";
import { useGlove } from "glove-react";

export default function Chat() {
  const { timeline, streamingText, busy, slots, sendMessage, renderSlot, renderToolResult } = useGlove();
  const [input, setInput] = useState("");

  return (
    <div>
      {timeline.map((entry, i) => (
        <div key={i}>
          {entry.kind === "user" && <p><strong>You:</strong> {entry.text}</p>}
          {entry.kind === "agent_text" && <p><strong>AI:</strong> {entry.text}</p>}
          {entry.kind === "tool" && (
            <>
              <p>Tool: {entry.name} — {entry.status}</p>
              {entry.renderData !== undefined && renderToolResult(entry)}
            </>
          )}
        </div>
      ))}
      {streamingText && <p style={{ opacity: 0.7 }}>{streamingText}</p>}
      {slots.map(renderSlot)}
      <form onSubmit={(e) => { e.preventDefault(); sendMessage(input.trim()); setInput(""); }}>
        <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
        <button type="submit" disabled={busy}>Send</button>
      </form>
    </div>
  );
}

サーバーサイドエージェント

CLI ツール、バックエンドサービス、WebSocket サーバー、またはブラウザー以外の環境では、glove-core を直接使用します。React、Next.js、ブラウザーは不要です。

最小限のセットアップ
import { Glove, Displaymanager, MemoryStore, createAdapter } from "glove-core";
import z from "zod";

// MemoryStore from glove-core is the default. Omit `store` from the
// Glove config and Glove constructs one for you with a generated identifier.
// For persistence implement your own StoreAdapter.
const store = new MemoryStore("my-session");

const agent = new Glove({
  store,
  model: createAdapter({ provider: "anthropic", stream: true }),
  displayManager: new Displaymanager(),  // required but can be empty
  systemPrompt: "You are a helpful assistant.",
  serverMode: true,  // canonical "I am headless" flag — drives default permission gating + MCP discovery policy
  compaction_config: {
    compaction_instructions: "Summarize the conversation.",
  },
})
  .fold({
    name: "search",
    description: "Search the database.",
    inputSchema: z.object({ query: z.string() }),
    async do(input) {
      const results = await db.search(input.query);
      return { status: "success", data: results };
    },
  })
  .build();

const result = await agent.processRequest("Find recent orders");
console.log(result.messages[0]?.text);
glove-core の MemoryStore

MemoryStore は、Glove を store なしで構築した場合に使用されるデフォルトの StoreAdapter です。メッセージ、トークン、ターン、タスク、権限、受信トレイ、createSubAgentStore といったすべてのオプション機能を実装しているため、サブエージェントをすぐに利用できます。

import { MemoryStore } from "glove-core";

const store = new MemoryStore("my-session");

// Sub-stores: durable: false (default) returns a fresh per-call store.
// durable: true caches the same instance per namespace so a subagent can
// carry message history across invocations.
const childStore = await store.createSubAgentStore("researcher", false);

MemoryStore はプロセス内でのみ有効で、再起動するとデータは失われます。データを永続化するには、独自のバックエンドに対応する StoreAdapter を実装してください。glove-sqlite は非推奨です。新しいプロジェクトでは、プロトタイピングには MemoryStore、本番環境には BYO StoreAdapter の使用をおすすめします。

React との主な違い
React(glove-react)サーバーサイド(glove-core)
render/renderResult を指定した defineTooldo のみを指定した .fold() — レンダラーは不要
useGlove() フックで状態を管理agent.processRequest() を直接呼び出す
GloveClient + GloveProvidernew Glove({...}).build()
createEndpointModel(SSE クライアント)createAdapter() またはアダプターを直接使用(例:new AnthropicAdapter())
glove-react の MemoryStoreglove-core の MemoryStore(デフォルト)— または永続化用の StoreAdapter を実装
表示を伴わないツール

ほとんどのサーバーサイドツールでは、表示マネージャーを使用しません。結果を返すだけです。

gloveBuilder.fold({
  name: "get_weather",
  description: "Get weather for a city.",
  inputSchema: z.object({ city: z.string() }),
  async do(input) {
    const res = await fetch(`https://wttr.in/${input.city}?format=j1`);
    return { status: "success", data: await res.json() };
  },
});

プレーンな文字列を返すこともできます。その場合は { status: "success", data: yourString } に自動的にラップされます。

インタラクティブなツール(pushAndWait)

ツールが display.pushAndWait() を呼び出すと、dm.resolve(slotId, value) が呼び出されるまでエージェントのループはブロックされます。これを UI レイヤー(WebSocket、ターミナル、Slack など)に接続します。

// Tool side
async do(input, display) {
  const confirmed = await display.pushAndWait({
    renderer: "confirm",
    input: { message: `Delete ${input.file}?` },
  });
  if (!confirmed) return { status: "error", data: null, message: "Cancelled" };
  // proceed...
}

// Server side — resolve when user responds
dm.resolve(slotId, true);
サブスクライバー(ログ記録、転送)
import type { SubscriberAdapter } from "glove-core";

const logger: SubscriberAdapter = {
  async record(event_type, data) {
    if (event_type === "text_delta") process.stdout.write((data as any).text);
    if (event_type === "tool_use") console.log(`\n[tool] ${(data as any).name}`);
  },
};

gloveBuilder.addSubscriber(logger);
よくあるパターン
  • CLI スクリプト:エージェントを構築し、processRequest() を呼び出して、結果を表示します
  • マルチターン REPL:readline を使ってループを回し、各 processRequest() の内容をストアに蓄積します
  • WebSocket サーバー:接続ごとに分離されたストア、dm、サブスクライバーを持つセッションを作成し、イベントを record() 経由で転送します
  • バックグラウンドワーカー:ジョブごとにエージェントを構築し、キューから処理します。表示は不要です
  • モデルのホットスワップ:実行時に agent.setModel(newAdapter) を呼び出します
  • MCP を利用するエージェント:serverMode: true を設定し、build() の前に mountMcp(glove, { adapter, entries }) を呼び出します。MCP 統合を参照してください。
オプションのストア機能
  • タスク(getTasks、addTasks、updateTask):glove_update_tasks ツールを自動登録します
  • 権限(getPermission(name, input?)、setPermission(name, status, input?)):requiresPermission: true(または (input) => boolean のゲート)を指定したツールは、同意を確認します。Executor は、ゲート付きの呼び出しごとにモデルから渡された入力を渡すため、ストアは入力ごとに判断を管理できます。デフォルトの MemoryStore は、エクスポートされている permissionKey(name, input) ヘルパーを使用して完全一致のキー付けを行います。
  • 受信トレイ(getInboxItems、addInboxItem、updateInboxItem、getResolvedInboxItems):glove_post_to_inbox ツールを自動登録します。インスタンス間の非同期通信が可能になります。

ストアがこれらを実装していない場合、機能は警告なしに無効になります。

受信トレイ(非同期メールボックス)

受信トレイを使用すると、エージェントは後で外部サービスによって解決されるリクエストを投稿できます。セッションやインスタンスをまたいで保持されます。

仕組み
  1. エージェントがタグ、リクエストテキスト、ブロッキングフラグを指定して glove_post_to_inbox を呼び出します
  2. アイテムはステータス pending でストアに永続化されます
  3. 外部サービスがアイテムを解決します(glove-sqlite の SqliteStore.resolveInboxItem() またはストア API 経由)
  4. 次回 agent.ask() が実行されると、解決済みアイテムがテキストメッセージとして挿入され、consumed にマークされます
  5. 保留中のブロッキングアイテムは、一時的なリマインダーとして表示されます(永続化されません)
  6. コンパクションでは、保留中の受信トレイアイテムがサマリーブロックに保持されます
組み込みツール:glove_post_to_inbox

ストアが受信トレイのメソッドを実装している場合、自動登録されます。入力スキーマ:

{
  tag: string,       // Category label, e.g. "restock_watch"
  request: string,   // Natural language description of what needs to happen
  blocking: boolean, // Default false. If true, agent should wait for resolution
}
外部からの解決
// From a background job, webhook handler, or cron:
import { SqliteStore } from "glove-sqlite";

SqliteStore.resolveInboxItem(
  "path/to/db.db",
  "inbox_item_id",
  "The item you requested is now available."  // text response
);

または、受信トレイ API ルートを設定している場合は REST 経由でも解決できます(coffee の例を参照)。

InboxItem 型
interface InboxItem {
  id: string;
  tag: string;
  request: string;
  response: string | null;
  status: "pending" | "resolved" | "consumed";
  blocking: boolean;
  created_at: string;
  resolved_at: string | null;
}
ストアメソッド(任意)
// Add to StoreAdapter to enable inbox:
getInboxItems?(): Promise<InboxItem[]>
addInboxItem?(item: InboxItem): Promise<void>
updateInboxItem?(itemId: string, updates: Partial<Pick<InboxItem, "status" | "response" | "resolved_at">>): Promise<void>
getResolvedInboxItems?(): Promise<InboxItem[]>

すべてのストア実装(glove-sqlite の SqliteStore、MemoryStore、createRemoteStore)は受信トレイに対応しています。

React との統合

useGlove() は tasks とともに inbox: InboxItem[] を返します。

const { inbox, tasks, timeline, sendMessage } = useGlove({ tools, sessionId });

// Show pending watches in UI
{inbox.filter(i => i.status === "pending").map(item => (
  <div key={item.id}>{item.tag}: {item.request}</div>
))}
リモートストアのアクション

createRemoteStore を使用する場合、バックエンドに永続化するための受信トレイアクションを追加します。

const storeActions: RemoteStoreActions = {
  // ...existing getMessages, appendMessages...
  getInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox`).then(r => r.json()),
  addInboxItem: (sid, item) => fetch(`/api/sessions/${sid}/inbox`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item }) }),
  updateInboxItem: (sid, itemId, updates) => fetch(`/api/sessions/${sid}/inbox/update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ itemId, updates }) }),
  getResolvedInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox/resolved`).then(r => r.json()),
};

拡張機能:フック、スキル、サブエージェント

processRequest はユーザーテキストから2種類のインラインディレクティブを解析し、モデルの呼び出し前にディスパッチします。サブエージェントは3つ目の拡張ポイントですが、ユーザーテキストからは解析されず、ディスパッチツールを介してモデルによってルーティングされます。ビルダーでは3つのメソッドを使ってハンドラーを登録できます(連鎖可能で、fold と同様に build() の後でも呼び出せます)。

トークン/メカニズム目的ビルダーメソッド
/hooknameエージェントの状態を変更する、コンパクションを強制する、モデルを切り替える、ターンを短絡させるdefineHook(name, handler)
/skillnameis_skill_injection: true が設定された合成ユーザーメッセージとしてコンテキストを挿入するdefineSkill({ name, handler, description?, exposeToAgent? })
glove_invoke_subagent({ name, prompt })(モデル側ツール)メインエージェントが自己完結型のタスクを振り分けられる子 Glove を登録します。ユーザーの @name テキストはそのままモデルに届きます。これはルーティングシグナルであり、解析されるディレクティブではありません。Claude Code のサブエージェント規約に準じています。defineSubAgent({ name, factory, description? })

/ トークンは、登録済みのフックまたはスキルと名前が一致する場合にのみバインドされます。/usr/local/bin はそのまま残ります。バインドされた / トークンは削除されず、[invoked_extension__hook_<name>] または [invoked_extension__skill_<name>] 形式の、再度トリガーされないプレースホルダーに置き換えられます。プレースホルダーは次回の解析では再バインドされませんが、永続化されたユーザーメッセージの構造を正確に保ち、拡張機能が実行されたことをモデルが確認できるようにします。@ トークンは glove によって解析されることはないため、[email protected] のようなメールアドレスは変更されずにモデルに届きます。

簡単な例
import { Glove, MemoryStore } from "glove-core";

const agent = new Glove({ /* ... */ })
  .defineHook("compact", async ({ controls }) => {
    await controls.forceCompaction();
  })
  .defineHook("stop", async () => ({
    shortCircuit: { message: { sender: "agent", text: "Cancelled." } },
  }))
  .defineSkill({
    name: "concise",
    description: "Tighter, snappier responses",
    exposeToAgent: true,
    handler: async ({ source, args }) =>
      `Be terse. (source=${source}, hint=${args ?? "none"})`,
  })
  .defineSubAgent({
    name: "weather",
    description: "Run the weather subagent. Use for weather questions.",
    factory: async ({ parentStore, parentControls, prompt }) => {
      const subStore = await parentStore.createSubAgentStore?.("weather", false)
        ?? new MemoryStore(`weather_${Date.now()}`);
      return new Glove({
        store: subStore,
        model: parentControls.glove.model,
        displayManager: parentControls.displayManager,
        systemPrompt: "You are a weather assistant. Answer the prompt and return.",
        compaction_config: { compaction_instructions: "Summarise weather lookups." },
      })
        .fold(weatherTool)
        .build();
    },
  })
  .build();

await agent.processRequest("/concise tell me about Rust");      // user-invoked skill
await agent.processRequest("/compact what's next?");           // hook → forceCompaction
// "@weather" reaches the model verbatim. The agent calls
// glove_invoke_subagent({ name: "weather", prompt: "NYC" }).
await agent.processRequest("@weather NYC");
フック
type HookHandler = (ctx: HookContext) => Promise<HookResult | void>;

interface HookContext {
  name: string;
  rawText: string;
  parsedText: string;        // text with bound tokens replaced by [invoked_extension__<type>_<name>] placeholders
  controls: AgentControls;
  signal?: AbortSignal;
}

interface HookResult {
  rewriteText?: string;      // override parsedText for downstream skills + the user message
  shortCircuit?:
    | { message: Message }
    | { result: ModelPromptResult };
}

interface AgentControls {
  context: Context;
  observer: Observer;
  promptMachine: PromptMachine;
  executor: Executor;
  glove: IGloveRunnable;
  store: StoreAdapter;             // direct access to the agent's StoreAdapter
  displayManager: DisplayManagerAdapter;  // direct access to the agent's display stack
  forceCompaction: () => Promise<void>;
}

forceCompaction は Observer.runCompactionNow() を呼び出します。これはトークンしきい値のチェックを除いた tryCompaction と同じ処理です。購読者には引き続き compaction_start/compaction_end が通知されます。

フックはドキュメント内の順序で実行されます。rewriteText は、後続のフック、スキル、最終的なユーザーメッセージに渡される作業テキストを上書きします。shortCircuit はユーザーメッセージを永続化してすぐに返し、モデルは呼び出しません。Glove は各フックハンドラーの実行直前に、購読者へ hook_invoked({ name })を通知します。

スキル
type SkillHandler = (ctx: SkillContext) => Promise<string | ContentPart[]>;

interface SkillContext {
  name: string;
  parsedText: string;        // when source = "user": user text with bound directives replaced by their placeholders. when source = "agent": same as args ?? "".
  args?: string;             // model-supplied free-form args (only when source = "agent")
  source: "user" | "agent";
  controls: AgentControls;
}

interface SkillOptions {
  description?: string;       // shown to the agent in the invoke-skill tool listing
  exposeToAgent?: boolean;    // default false
}

interface DefineSkillArgs extends SkillOptions {
  name: string;
  handler: SkillHandler;
}

interface RegisteredSkill {
  handler: SkillHandler;
  description?: string;
  exposeToAgent: boolean;
}

スキルによって挿入されたメッセージでは、既存の is_compaction および is_compaction_request フラグとともに、Message に is_skill_injection: true が設定されます。トランスクリプトレンダラーでこのフラグを使うと、挿入されたコンテキストを実際のユーザーターンとは異なる方法で表示できます。Glove は購読者へ skill_invoked({ name, source: "user" | "agent", args? })を通知します。ユーザー側のディレクティブでは Glove.processRequest からディスパッチされ、エージェント側の呼び出しでは glove_invoke_skill ツール内からディスパッチされます。

エージェントにスキルを公開する

exposeToAgent: true を設定すると、Glove はエグゼキューターに単一の glove_invoke_skill ツールを自動登録します。このツールの説明には公開されているすべてのスキル(- name — description)が列挙され、新しい公開スキルが定義されるたびにその場で再構築されるため、build() 後に登録されたスキルもすぐに反映されます。

agent.defineSkill({
  name: "research-mode",
  description: "Switch to long-form research mode with citations",
  exposeToAgent: true,
  handler: async ({ source, args, parsedText }) => {
    if (source === "agent") {
      // Agent invoked via glove_invoke_skill — `args` is the model-supplied string.
      return `Switch into research mode. Focus: ${args ?? "general"}.`;
    }
    // source === "user" — `parsedText` is the user message after directive substitution
    // (e.g. "[invoked_extension__skill_research-mode] tell me about ribosomes").
    return `Switch into research mode. User said: ${parsedText}`;
  },
});

// User: "/research-mode tell me about ribosomes"
//   → source = "user", parsedText = "[invoked_extension__skill_research-mode] tell me about ribosomes"
// Agent: glove_invoke_skill({ name: "research-mode", args: "ribosome assembly" })
//   → source = "agent", args = "ribosome assembly"

成功時、このツールは { status: "success", data: { skill, content } } を返します。不明な名前、または公開されていない名前の場合は、{ status: "error", message: 'Skill "..." is not available', data: null } を返します。スキルが ContentPart[] を返す場合、テキストパーツは data.content に結合され(モデルから参照可能)、パーツの完全なリストは renderData に保持されます(MCP-bridge の規約と同様に、クライアントのレンダラーから参照可能)。

項目ユーザーの /skillエージェントの glove_invoke_skill
配置先実際のターンの前に合成ユーザーメッセージとして配置(is_skill_injection: true)エージェントの tool_use に対するツール結果
SkillContext.source"user""agent"
SkillContext.argsundefinedモデルが指定した自由形式の文字列
exposeToAgent による制限いいえ — ユーザーが呼び出す場合は常に動作しますはい — 公開されたスキルのみ呼び出せます
サブエージェント

Claude Code のサブエージェント規約を参考にしています。サブエージェントを定義すると、メインエージェントが { name, prompt } を渡して呼び出す glove_invoke_subagent ツール(core からエクスポートされる定数 SUBAGENT_DISPATCH_TOOL_NAME)が自動登録されます。元のメッセージ内にあるユーザーの @name テキストは glove によって解析されず、そのままモデルに渡され、ルーティングの合図として機能します。ファクトリーは呼び出しごとに新しい子 Glove を作成し、ディスパッチャーがそれを実行します。

interface SubAgentFactoryContext {
  /** Subagent name as registered with `defineSubAgent`. */
  name: string;
  /** The task prompt the parent agent supplied when calling `glove_invoke_subagent`. */
  prompt: string;
  /** The parent agent's store. Use `createSubAgentStore(name, durable)` to derive a child store. */
  parentStore: StoreAdapter;
  /** Full parent agent controls (context, observer, promptMachine, executor, glove, store, displayManager, forceCompaction). */
  parentControls: AgentControls;
}

type SubAgentFactory = (
  ctx: SubAgentFactoryContext,
) => Promise<IGloveRunnable> | IGloveRunnable;

interface SubAgentOptions {
  /** Short description shown to the agent in the invoke-subagent tool listing. */
  description?: string;
}

interface DefineSubAgentArgs extends SubAgentOptions {
  name: string;
  factory: SubAgentFactory;
}

interface RegisteredSubAgent {
  factory: SubAgentFactory;
  description?: string;
}
標準的なファクトリー
import { Glove, MemoryStore } from "glove-core";

glove.defineSubAgent({
  name: "researcher",
  description: "Deep research subagent",
  factory: async ({ parentStore, parentControls }) => {
    // Sub-store: durable false → fresh per-call; durable true → cached for the namespace.
    const subStore = await parentStore.createSubAgentStore?.("researcher", false)
      ?? new MemoryStore(`researcher_${Date.now()}`);

    return new Glove({
      store: subStore,
      model: parentControls.glove.model,           // inherit the parent's model
      displayManager: parentControls.displayManager, // share the parent's display stack
      systemPrompt: "You are a researcher.",
      compaction_config: { compaction_instructions: "Summarize research progress." },
    })
      .fold(searchTool)
      .fold(fetchTool)
      .build();   // build() can take a store too; passing it here is equivalent to constructor `store`
  },
});

ディスパッチャーは実行中、親のサブスクライバーを子にアタッチし、child.processRequest(prompt, signal) を呼び出して(親の中断シグナルを転送して)、その後デタッチします。子の最終的なエージェントテキストがツール結果として返されます。

ツール結果の形式

glove_invoke_skill と対称的です。

// Tool input
{ name: string, prompt: string }

// Success
{ status: "success", data: { subagent: string, content: string } }

// Unknown name
{ status: "error", message: 'Subagent "..." is not registered. Use one of: ...', data: null }

// Factory threw
{ status: "error", message: 'Subagent "..." factory threw: ...', data: null }

// Child run threw
{ status: "error", message: 'Subagent "..." failed: ...', data: null }
対応イベント — 1 対 1 の対称性を保証

ディスパッチャーではなく Executor が、すべての glove_invoke_subagent 呼び出しを subagent_invoked({ name, prompt })で実行前に囲み、subagent_completed({ name, status: "success" | "error", message? })で実行後に囲みます。親の中断によってディスパッチャーの Promise チェーンが途中で終了する場合でも、この対応関係は対称です。Executor の中断ハンドラーが終了イベントを必ず発行します。この間に子の Glove が発行するイベントはそのサブエージェントに属します。実行中は親のサブスクライバーが子にアタッチされます。

よくあるパターン
  • 呼び出しごとに新しい子を作成:ファクトリーは呼び出しのたびに、独自の MemoryStore(または永続化しないサブストア)を持つ新しい Glove を作成します。デフォルトであり、推奨される方法です。
  • 永続化する子:parentStore.createSubAgentStore("name", true) は名前空間に対して同じストアを返すため、サブエージェントは呼び出しをまたいでメッセージ履歴を保持します。ファクトリーは引き続き新しい Glove を作成し、再利用されるのはストアのみです。
  • 1 つのメッセージで複数指定("@reviewer @architect please discuss this design"):どちらの名前もモデルに渡されます。モデルはサブエージェントごとに glove_invoke_subagent を 1 回ずつ呼び出せます(逐次実行、または別々のツール呼び出しによる並列実行を選択できます)。
サブストア:createSubAgentStore

StoreAdapter は、任意のファクトリーメソッド createSubAgentStore(namespace: string, durable?: boolean): Promise<StoreAdapter> を公開します。

  • durable: false(デフォルト)→ 呼び出しごとに新しい分離されたストアを返します。サブエージェントは呼び出しのたびにコンテキストがゼロの状態から開始します。
  • durable: true → 同じ名前空間には同じインスタンスが返されるため、サブエージェントは親の存続期間中、呼び出しをまたいでメッセージ、タスク、トークン、カウンターを保持します。

glove-core の MemoryStore は両方のモードを実装しています。ストアが createSubAgentStore を実装していない場合は、ファクトリー内で new MemoryStore(...) にフォールバックしてください。これにより「呼び出しごとに新規作成」の動作を維持できます。

setDisplayManager(チェーン可能)

Glove(ビルダー形式と実行形式の両方)は、setDisplayManager(displayManager) を公開しています。サブエージェントファクトリーは通常、parentControls.displayManager を子のコンストラクターに渡しますが、ビルド後に child.setDisplayManager(parentControls.displayManager) を呼び出して、途中から有効にすることもできます。チェーン用に this を返します。

processRequest のディスパッチ順序
  1. 生テキストから / ディレクティブを解析します(正規表現 (^|\s)\/([A-Za-z][\w-]*)(?=\s|$))。バインド済みのトークンは [invoked_extension__<type>_<name>] プレースホルダーに置き換えられ、未バインドのトークンはそのまま残ります。@ トークンは解析されません。
  2. ドキュメントの順序でフックを実行します。Glove はフックごとに hook_invoked を出力します。rewriteText を適用し、最初の shortCircuit を尊重して返します。
  3. スキルをマテリアライズします(source: "user")。それぞれが合成ユーザーメッセージとなり、実際のメッセージの前に context.appendMessages を介して永続化されます。Glove はスキルごとに skill_invoked を出力します。
  4. プレースホルダーに置き換えたテキスト(変更されていない @mention を含みます)と、呼び出し元から渡されたテキスト以外の ContentParts を使って、実際のユーザー Message を構築します。
  5. Agent.ask に引き渡します。サブエージェントの呼び出しは、エージェントループ内で glove_invoke_subagent ツール呼び出しを介して行われ、エグゼキューターは各呼び出しを subagent_invoked / subagent_completed で囲みます。
is_skill_injection フラグ

スキルからマテリアライズされたユーザーメッセージでは、Message に is_skill_injection: true が設定されます。トランスクリプトのレンダリングには is_compaction と組み合わせて使用します。注入されたメッセージを折りたたむ、ミュートする、またはフィルタリングして、実際のユーザーターンと視覚的に区別できます。

Message の pre_modified_text

フックが rewriteText を介してユーザーメッセージを書き換えた場合、モデルに渡された書き換え後のバージョンと並べて、ユーザーが実際に入力した内容をフロントエンドで表示できるよう、元の生テキストは Message.pre_modified_text に保持されます。

公開 API の範囲
import {
  // Builder
  Glove, // .defineHook(), .defineSkill(), .defineSubAgent()
  // Types
  HookHandler, HookContext, HookResult,
  SkillHandler, SkillContext, SkillOptions, DefineSkillArgs, RegisteredSkill,
  SubAgentFactory, SubAgentFactoryContext, SubAgentOptions, DefineSubAgentArgs, RegisteredSubAgent,
  AgentControls,
  // Constants
  SUBAGENT_DISPATCH_TOOL_NAME, // "glove_invoke_subagent"
  // Helpers
  parseTokens, formatSkillMessage,
  createSkillInvokeTool, renderSkillToolDescription,
  createSubAgentInvokeTool, renderSubAgentToolDescription,
} from "glove-core";

メインエントリーと glove-core/extensions サブパスから利用できます。

MCP 統合(glove-mcp)

glove-mcp は、Model Context Protocol サーバー(Notion、Gmail、Linear、Slack、自社 API 用の内部 MCP ラッパーなど)を Glove エージェントに接続し、それらのツールを通常の Glove ツールとしてモデルのツールリストに表示します。v1 では Streamable HTTP トランスポートのみをサポートします。

使用する場面
  • ベンダーがすでに MCP 経由で公開しているサードパーティ機能(Notion、Gmail、Linear、Slack、Zapier-MCP など)が必要な場合。
  • 複数の内部サービスを利用しており、それらを一貫した単一の統合形式で扱いたい場合。
  • 起動時にすべてのツールを接続するのではなく、会話の途中でエージェントに機能を発見・有効化させたい場合。

両方のエンドを自分で管理していて、ファーストパーティのツールがいくつか必要なだけなら、手書きの glove.fold(...) のほうがシンプルです。カタログが大規模な場合や、サーバーが自分の管理下にない場合に、MCP を導入する価値があります。

メンタルモデル:カタログ+アダプター

意図的に分離された2つの要素があります。

  • McpCatalogueEntry[] — アプリケーションレベルで定義する静的リストです。アプリがサポートするMCP サーバーごとに1つのエントリーがあり、id、name、description、url、tags?、excludeTools?、metadata?を含みます。ユーザー間で共通です。idはツールの名前空間プレフィックスと有効化キーを兼ねます。excludeTools?: string[]を指定すると、名前空間が付いていない正確な名前でツールを除外します。接続時にフィルタリングされるため、除外されたツールは、起動時の再読み込み、discovermcpによる有効化、同じ接続を介したglove-scratchpadブリッジなど、どの経路からもマウントされません。

  • McpAdapter — コンシューマーが実装する、会話ごとのインターフェースです(StoreAdapterに相当します)。会話で有効になっているサーバーIDを保持し、アクセストークンを解決します。

    interface McpAdapter {
      identifier: string;                          // for log correlation
      getActive(): Promise<string[]>;              // ids active in this conversation
      activate(id: string): Promise<void>;         // called by the discovery subagent
      deactivate(id: string): Promise<void>;       // for the consumer's UI; v1 limitation: doesn't unload tools
      getAccessToken?(id: string): Promise<string>;                // bearer seam — return a token string
      getAuthHeaders?(id: string): Promise<Record<string, string>>; // custom-header seam — e.g. { "x-api-key": ... }; wins over getAccessToken
    }
    

認証の接続点はgetAccessTokenとgetAuthHeadersだけです。一般的なケースではgetAccessTokenを使います。フレームワークが返された文字列をAuthorization: Bearer ...に設定します。ベアラートークンを受け付けないサーバー(Composioのx-api-keyなど)では、getAuthHeadersを実装してヘッダー一式を返してください。両方が定義されている場合はgetAuthHeadersが優先され、どちらも定義されていない場合、認証ヘッダーなしで接続します。認証情報の取得、更新、永続化はすべてコンシューマーの責任です。環境変数、ボルト、独自のOAuthフロー、オプトインのrunMcpOAuthヘルパーなど、いずれも使用できます。

mountMcp — 標準のエントリーポイント

new Glove(...)の後、glove.build()の前に呼び出します。

import { mountMcp } from "glove-mcp";

const glove = new Glove({ /* ... */ , serverMode: true });

await mountMcp(glove, {
  adapter,                                  // McpAdapter
  entries,                                  // McpCatalogueEntry[]
  ambiguityPolicy: { type: "auto-pick-best" },  // optional
  subagentModel: undefined,                 // optional — defaults to glove.model
  subagentSystemPrompt: undefined,          // optional — defaults to per-policy prompt
  clientInfo: { name: "My App", version: "1.0.0" },  // optional
  filterTools: (tool, entry) => !tool.annotations?.destructiveHint,  // optional — catalogue-wide drop
});

glove.build();

実行内容は次のとおりです。

  1. adapter.getActive()を読み取り、有効な各IDに対してMCP接続を開き(getAccessTokenを使用)、ツールを一覧表示して、それぞれをbridgeMcpTool経由でメインエージェントに組み込みます。サーバーごとの再読み込みに失敗した場合はログに記録してスキップします。一時的な障害でエージェントが停止することはありません。
  2. glove.defineSubAgent(discoverySubAgent({...}))を使って、discovermcp探索用サブエージェントを登録します。これにより、モデルは会話中に追加のサーバーを有効化するよう依頼できます。モデルはglove_invoke_subagent({ name: "discovermcp", prompt: "..." })で呼び出します。

ツールを除外する場合。 特定のツールをモデルに公開しないようにするには、McpCatalogueEntryでexcludeTools: string[](名前空間が付いていない正確な名前)を指定するか、カタログ全体に適用するルールとしてmountMcpにfilterTools: (tool, entry) => booleanを渡します(各エントリーのexcludeToolsに加えて適用されます)。どちらも接続時に適用されるため(connectMcpもexcludeTools/filterToolsを直接受け付けます)、起動時の再読み込み、discovermcpによる有効化、同じ接続上に構築されたglove-scratchpadブリッジ(mcpResources/fnsFromMcp)など、すべてのマウント経路で一元的に除外できます。フィルタリングされるのは一覧表示のみです。conn.raw/conn.callToolには影響しません。includeTool(tool, { excludeTools, filterTools })はエクスポートされている純粋な述語関数です。

mountMcpは、再読み込みとサブエージェントの登録が完了すると戻ります。最もすっきりした初期化順序にするにはbuild()の前に呼び出してください。ただし、build()の後にfold()/defineSubAgent()を呼び出しても動作します。

ブリッジされたツールの形式

bridgeMcpTool(connection, tool, serverMode) は、次の規則に従う GloveFoldArgs を生成します。

  • 名前:${entry.id}__${tool.name}(例:notion__search)。区切り文字の __(MCP_NAMESPACE_SEP としてエクスポート)は、すべてのモデルプロバイダーで正規表現に安全です。
  • 入力スキーマ:MCP サーバーから取得した生の JSON Schema を jsonSchema 経由で渡します(Zod は使用しません)。MCP サーバーが信頼できる情報源です。
  • 結果の形式:サーバーが outputSchema(MCP 2025-06-18 以降)を宣言している場合、コンパクトな Returns: … 形式(エクスポートされた jsonSchemaToShape 経由)をツールの説明に追加します。モデルのツール呼び出しのワイヤー形式は入力専用であるため、通常のブリッジ経由で戻り値の形式を提示できるのは説明だけです。この改訂より古いサーバー、またはスキーマを宣言していないサーバーでは追加されません。
  • requiresPermission:serverMode では常に false です。それ以外では、MCP ツールが readOnlyHint: true の注釈を付けていない限り true です。
  • 結果:サーバーが structuredContent を返す場合(MCP 2025-06-18 以降)はそれをモデルに表示し、data に JSON 文字列として格納します。それ以外の場合は、結合した content[] のテキストを表示します。React レンダラーで使用できるよう、常に content[] 全体を renderData として渡します。
  • 認証期限切れの契約:callTool 中に発生した 401 形式のエラーは、すべて { status: "error", message: "auth_expired", data: null } にマッピングされます。会話ログからこれを検出してトークンを更新してください。次の呼び出しでは、getAccessToken 経由で新しい値が取得されます。
検出(discovermcp)と曖昧さのポリシー

mountMcp は、モデルが呼び出し先として選択できる単一のサブエージェント、discovermcp を登録します。モデルは glove_invoke_subagent({ name: "discovermcp", prompt: "send an email" }) を使って呼び出します。ファクトリーは子 Glove を作成し(専用のサブストアとして parentStore.createSubAgentStore("discovermcp", false) を使用し、サブストアに対応していない場合はプライベートな DiscoveryMemoryStore にフォールバックします。メインエージェントのモデル、displayManager、serverMode を継承します)、次のツールを子に追加します。

  • list_capabilities(query?, tags?) — カタログを部分一致検索します。
  • activate(id) — 接続し、メインエージェントにツールをブリッジして、有効な状態を永続化します。ツールはメインモデルの次のターンから使用できます。
  • deactivate(id) — 永続化された状態を切り替えます。v1 の制限として、セッションを更新するまでツールは読み込まれたままです。
  • ask_user(question, options) — interactive ポリシーの場合のみ登録されます。メインの displayManager 上で mcp_picker レンダラーを使って表示します。

曖昧さのポリシーは、サブエージェントが一致する可能性のある候補を複数見つけた場合の動作を制御します。

ポリシー動作使用する場面
{ type: "interactive" }サブエージェントが pushAndWait 経由で ask_user を呼び出します。displayManager に mcp_picker レンダラーが必要です。レンダラーが接続されたブラウザー UI/チャットアプリ。serverMode: false の場合のデフォルトです。
{ type: "auto-pick-best" }サブエージェントが常に最も順位の高い候補を選びます。人間の介入はありません。ヘッドレス/サーバーサイド/CLI。serverMode: true の場合のデフォルトです。
{ type: "defer-to-main" }サブエージェントが候補のリストをテキストとして返し、メインエージェントに有効化する対象を決定させます。メインモデルがサブエージェントよりも多くの会話コンテキストを持つ、複数の MCP を対象とした検出フロー。

Glove 設定の serverMode: true は、「ヘッドレスである」ことを示す標準的なフラグです。デフォルトの曖昧さポリシーと、ブリッジされたツールのデフォルトの requiresPermission(常にゲートしない)の両方を決定します。

認証モデル — 静的ヘッダー(ベアラーまたはカスタム)

フレームワークが認識するのは、接続ごとの静的ヘッダーのみです。connectMcp には2つのヘルパーが用意されています。Authorization: Bearer ... 用の auth: bearer(token | () => token) と、カスタムヘッダー(例:Composio の x-api-key)を必要とするサーバー用の auth: headers(map | () => map) です。どちらも、接続ごとに再解決される関数を受け取れます。mountMcp と検出用の activate ツールは、adapterAuth(adapter, id) を使って認証情報を解決します。getAuthHeaders が定義されていればそれを使い、そうでなければ getAccessToken をベアラーとしてラップし、それもなければ認証ヘッダーを付与しません。

import { bearer, headers, connectMcp } from "glove-mcp";

const conn = await connectMcp({
  namespace: "notion",
  url: "https://mcp.notion.com/mcp",
  auth: bearer(() => adapter.getAccessToken!("notion")),
  clientInfo: { name: "My App", version: "1.0.0" },
});

const composio = await connectMcp({
  namespace: "composio",
  url: "https://mcp.composio.dev/...",
  auth: headers({ "x-api-key": process.env.COMPOSIO_API_KEY! }),
});
auth_expired の仕様

呼び出しの途中でトークンの有効期限が切れると、ブリッジされたツールの結果に { status: "error", message: "auth_expired" } として現れます。フレームワークはトークンを更新しません。アプリ側で次の処理を行う必要があります。

  1. 会話ログ上の auth_expired を検出します(サブスクライバーの tool_use_result イベント、または事後処理)。
  2. 認証情報を管理している仕組みを使って、トークンを更新するか、再認証します。
  3. ストアを更新します。次のブリッジ呼び出しでは、getAccessToken から新しいトークンが取得されます。

UI を利用する場合、通常は「Notion に再接続してください」というトーストを表示します。CLI では、ユーザーに認証コマンドの再実行を案内するのが一般的です。

glove-mcp/oauth — オプトインの OAuth ツール群

OAuth フローをまだ用意していない場合、glove-mcp/oauth サブパスには、MCP の認可仕様に基づく小規模なリファレンス実装が含まれています。

  • runMcpOAuth(opts) — 1回の呼び出しで完結するフローです。http://localhost:53683/callback(変更可能)でローカルリスナーを起動し、SDK を通じて DCR を実行します(preRegisteredClient を指定すると省略できます)。ユーザーのブラウザーを開き、コードをトークンと交換したうえで、listTools(または任意の callTool)を使って検証します。examples/mcp-cli/*-mcp-auth.ts スクリプトで使用されています。
  • FsOAuthStore / MemoryOAuthStore — OAuthStore の実装です。FsOAuthStore は、モード 0600 の単一の JSON ファイルに書き込み、一時ファイルとリネームを使ってアトミックに保存します。本番環境では、独自の DB に置き換えてください。
  • McpOAuthProvider — SDK から直接 auth() を実行する上級ユーザー向けの、低レベルな OAuthClientProvider です。
  • buildClientMetadata、MCP_DEFAULT_CLIENT_INFO、emptyOAuthState — 小規模なヘルパーです。

トークンをすでに持っているユーザー(環境変数、内部連携、ボールト、既存の OAuth セットアップなど)は、このサブパス全体を無視できます。getAccessToken はベアラーを返すだけです。

型シグネチャの詳細については、api-reference.md — glove-mcp/oauth を、具体的な使用例については、examples.md — パターン:MCP OAuth フロー を参照してください。

本番環境への移行

リファレンスの examples/mcp-cli セットアップは、単一ユーザー向けの Node CLI です。本番環境では通常、次の対応が必要です。

  • マルチユーザーストア — FsOAuthStore を、DB をバックエンドとするユーザーごとの OAuthStore に置き換えます。インターフェースは get、set、delete の3つのメソッドです。
  • ルートハンドラー内の OAuth フロー — GET /oauth/<id>/start で runMcpOAuth(または低レベルの SDK auth())を呼び出し、GET /oauth/<id>/callback で完了させます。同じ仕組みを別の方法で呼び出します。runMcpOAuth のローカルリスナーを使う方式は CLI には便利ですが、ロードバランサーの背後で使う用途には適していません。
  • バックグラウンドでの更新 — 使用しているスタックの方法で期限切れトークンを更新します。getAccessToken は最新のベアラー文字列を読み取るだけです。
  • アクティブ状態の永続化 — 例で示されている McpAdapter は、アクティブな ID の管理にインメモリの Set を使用しています。本番環境では、再起動後の再読み込みが実際に機能するよう、アクティブな ID を会話ごとに(メッセージと一緒に)永続化してください。

エージェントのコード自体を変更する必要はありません。接続箇所は McpAdapter.getAccessToken だけです。

クイックリファレンス — 各機能の場所
必要な機能シンボル
エージェントに MCP をマウントするmountMcp(glove, { adapter, entries, ... })
コンシューマーアダプターを実装するMcpAdapter インターフェース
カタログエントリーを作成するMcpCatalogueEntry
その場で接続する(事前確認、カスタムフロー)connectMcp({ namespace, url, auth })
ツールを手動でブリッジするbridgeMcpTool(connection, tool, serverMode)
Bearer ヘッダーのヘルパー`bearer(token
カスタムヘッダー(Bearer 以外、例:x-api-key)のヘルパー`headers(map
アダプターから認証リゾルバーを取得するadapterAuth(adapter, id)
ディスカバリーサブエージェントのファクトリーdiscoverySubAgent({ adapter, entries, ambiguityPolicy })(DefineSubAgentArgs を返します。glove.defineSubAgent(...) に渡してください)
ツールの名前空間セパレーターMCP_NAMESPACE_SEP("__")
生の接続で 401 を検出するUnauthorizedError
OAuth フローを実行するglove-mcp/oauth の runMcpOAuth(opts)
OAuth の状態を永続化するglove-mcp/oauth の FsOAuthStore、MemoryOAuthStore
クライアントメタデータを構築するglove-mcp/oauth の buildClientMetadata(opts)

メモリー(glove-memory)

5 つの独立したサブシステムを備えた、スキーマ優先のメモリーレイヤーです。ストレージに依存しないアダプターコントラクトを提供し、開発/テスト用の参照インメモリーアダプターが付属します。ステータス:ドラフト v0.1。対応するストレージバックエンド(glove-memory-sqlite、glove-memory-postgres)はまだリリースされていません。

5 つのサブシステム
サブシステムアダプター用途
エンティティEntityMemoryAdapterグラフ構造でスキーマ優先の、決定論的なアイデンティティ解決を行います。ノード(人物、組織、プロジェクト)と、それらの間の型付きエッジを扱います。キュレーターが記述し、エージェントが読み取ります。
エピソードEpisodicMemoryAdapterタイムラインに紐づいた追記専用のイベントです。会議、決定、観察を扱います。時間を第一級のフィールドとして扱い、セマンティック検索はオプトイン方式です(supportsSemanticSearch で対応状況を通知します)。
リソースResourceFsAdapterエージェントが ls/read/grep/glob/edit で操作する、POSIX スタイルの仮想ファイルシステムです。調査メモ、文字起こし、リンク集を保存します。テキストのみを扱い、絶対パスのみを使用します(./.. は使用できません)。
コンテキストContextAdapterユーザーが設定した周辺コンテキストを、各モデルの反復処理の前に一時的な実行時コンテキストとして追加します。他とは異なる形式で、キュレーターによる抽出はなく、読み手/キュレーターの分離もありません。1 回の登録で、エージェントに読み取りツールと書き込みツールの両方を提供します。
フォームFormAdapter会話を通じて構造化データを収集します。定義はコード(Zod とクロージャ)で記述され、一度に 1 つの階層を読み込みます。そのため、60 フィールドのフォームも 6 フィールドのフォームと同じ長さのプロンプト行で維持できます。
アーキテクチャに関する推奨事項:メインの Glove にメモリツールを詰め込まない

メモリへのアクセスが必要なエージェントを構築する場合、エンティティ/エピソード/リソースのツールをメインの Glove に直接追加しないでください。取得タスクごとにサブエージェントを構築し、メインエージェントに登録します。各サブエージェントには、必要なアダプターの機能だけを追加します。

理由:

  • プロンプトの範囲を限定できます。 ツールの説明には、その役割に必要なスキーマの一部だけが表示されるため、トークンコストはオントロジー全体の規模ではなく、役割に応じて変わります。
  • ルーティングを明確にできます。 lookup/recall/find-notes といったサブエージェント名自体が、推論の手がかりになります。「メモリツールが8つあります。どれを使うか決めてください」よりも、意図を絞り込めます。
  • 変更範囲は構造的に制限されます。 useMemoryReader を追加したサブエージェントは書き込みできません。そのための機能自体がありません。さらに細かく、あるフォルダーは読み取り可能だが書き込み不可にする、ファイルを整理するが削除はしないキュレーターにする、といった設定は、{ tools: { allow, deny } } と withResourceAccess を組み合わせます。
  • アダプターは共有されたままです。 すべてのサブエージェントが同じ基盤のグラフ/タイムライン/ファイルシステムに対して読み書きします。ツールを分けても、データが分割されるわけではありません。

キュレーター側も同様です。書き込み側の専門サブエージェント(entity-linker、episode-recorder、resource-filer)にルーティングする親キュレーターのほうが、すべての書き込みツールを追加した単一のキュレーターより優れています。

例外は useContext です。Context は小規模(ツールは4つ)で、ユーザー主導(「これを覚えておいて……」)です。また、実際にユーザーが会話するエージェントに配置する必要のある runtime-context プロバイダーが付属しています。useContext はメインエージェントに配置してください。

具体的なパターンについては、examples.md — メモリ:サブエージェントに委任するリーダー/キュレーターの構成/コンテキストのフローを参照してください。

ヘルパーの種類

各ヘルパーは、関連するツール群を Glove に追加します。すべてチェーン用に同じ G を返し、IGloveBuilder または IGloveRunnable(fold を公開するもの)に対して動作します。

ヘルパー追加するツール備考
useMemoryReader(glove, adapter)glove_memory_find、_get、_queryエンティティグラフへの読み取り専用アクセスです。
useMemoryCurator(glove, adapter)リーダーツール + _add_node、_update_node、_connect、_disconnect、_merge_nodesエンティティへの完全な書き込みアクセスです。
useEpisodicReader(glove, adapter)glove_episodic_find、_timeline、_search_search は adapter.supportsSemanticSearch === true の場合にのみ登録されます。
useEpisodicCurator(glove, adapter)リーダーツール + _record、_update、_delete
useResourcesReader(glove, adapter)glove_resources_ls、_read、_stat、_grep、_glob、_search、_links_for_search は supportsSemanticSearch の場合にのみ登録されます。
useResourcesCurator(glove, adapter)リーダーツール + _write、_edit、_mkdir、_move、_remove、_set_metadata
useContext(glove, adapter)glove_context_get、_set、_update、_unset各モデルの反復処理の前に adapter.render() を呼び出し、入力末尾に一時的なユーザー役割のスナップショットを追加します。システムプロンプトと保存済みの履歴は変更されません。

すべてのヘルパーは、末尾に任意の { tools?: { allow?, deny? } } を受け取ります(useFormRunner では、その設定項目を config に指定します)。これにより、実際に追加されるツールを絞り込めます。詳しくは以下をご覧ください。

階層化メモリ — 共有領域とプライベート領域をひとつのビューに

メモリはいくつかの階層に分かれています。組織のハンドブック、共通のオントロジー、公開済みのイベント、継続的な指示など、別の場所で作成された共有情報のうち、エージェントが読み取りはできても変更してはならないものもあれば、エージェント自身の情報もあります。これらは異なるストアに保存されます。共有コーパスを各プライベートストアにコピーすることはできませんし、エージェントがその違いを把握する必要もありません。各 layer* 関数はスタックを受け取り、通常のコントラクトに準拠したひとつのアダプターを返します。そのため、既存の use* ヘルパーで通常のツール群を追加できます。

const resources = layerResources([
  { name: "handbook", adapter: sharedFs,  access: "read", paths: ["/handbook"] },
  { name: "notes",    adapter: privateFs, access: "write" },
]);
useResourcesCurator(glove, resources);

4 つのサブシステムはすべてレイヤー化できます。layerEntity、layerEpisodic、layerResources、layerContext(glove-memory/layered)。どこでも共通するルール:スタック内の access: "write" の階層は必ず 1 つだけにします(0 個だとすべての書き込みツールが使えず、2 個だとルーティングが曖昧になり、どちらも構築時にエラーが発生します)。読み取りはレイヤーの順序に従ってマージされます(衝突時は先にあるものが優先されるため、順序そのものがシャドーイングのルールです)。書き込みは対象を所有する階層にルーティングされ、拒否される場合は MemoryLayerError(code: "layer_read_only"。レイヤー名を示します)が発生します。limit/offset はマージ後の結果に適用されます(各階層にはオフセットなしで limit + offset を要求します。先行レイヤーでスキップされた行と、マージ後のビューでスキップされる行は異なるためです)。setEmbedding は読み取り専用の階層に対しても実行できます。インデックス作成はエージェントではなくホストの役割だからです。layerOf(id|path) を使うと、ホストは対象がどの階層に由来するかを確認できます。

リソース — レイヤーの順序が優先順位となり、paths はレイヤーの対象範囲をサブツリーに限定します。この 1 つの仕組みで、マウント(互いに重ならないプレフィックス)とユニオン(どちらも / 全体を対象とし、プライベートを先に指定して共有をシャドーイング)の両方に対応します。パスは変換されません。/handbook を対象とするレイヤーは、アダプターに同じ絶対パスを渡します。そのため、共有ストアはそのプレフィックス配下に配置する必要があります。パスを変換すると、保存済みの metadata.links のリンク先がすべて暗黙のうちに無効になります。階層が自分の対象範囲外にデータを漏らすことはありません(結果は、その階層が所有すると申告した範囲に絞り込まれます)。ただし、マウント先に到達できるよう、対象プレフィックスまでの途中にあるディレクトリは一覧表示できます。書き込みはパスをすでに保持している階層にルーティングされ、新しいパスの場合はプレフィックスの所有者にフォールバックします。この順序により、remove("/handbook/pay.md") はプライベートストアを先に試した場合に返る「見つかりません」ではなく、読み取り専用の階層であることを報告します。コピーオンライトには対応していません。階層をまたぐ移動と、共有階層に到達する再帰的な削除は、どちらも拒否されます。

エンティティ — 知っておくべき点は 2 つあります。addNode は書き込み前に、クラスの identityKeys を使って読み取り専用階層内の ID を解決し、共有ノードの ID を created: false とともに返します。そうしないと、共有エンティティが初めて言及されるたびに、各エージェントがプライベートな重複を作成し、共有グラフが気付かないうちに分岐してしまうためです。その結果、返された ID は変更できない可能性があり、続けて updateNode を呼び出すと拒否されます。また、エッジを階層間にまたがらせることはできません。connect は単一のアダプター内で両方のエンドポイントを検証しますが、プライベートストアには共有ノードの行がないため、階層をまたぐ connect は code: "cross_layer_unsupported" で拒否されます。回避策はあります。エピソードの participants とリソースの metadata.links は検証されない単純な ID なので、階層を自由にまたげます。これが、グラフをレイヤー化する際に実際に情報が失われる唯一の部分です。

エピソードは、階層をインターリーブして 1 つの真の時系列を構成します。いずれかの階層がセマンティック検索に対応していれば、supportsSemanticSearch は true になり、searchEpisodes は対応している階層だけを検索します(マージ後のランキングはベストエフォートでインターリーブされます。独立して構築された 2 つのインデックスのスコアは比較できないためです)。コンテキストは読み取りをマージし、各階層の render ブロックを共有を先にして連結します。setSection は書き込み可能な階層のエントリだけを置き換えます。

レイヤー化は withResourceAccess と組み合わせるものであり、置き換えるものではありません。レイヤー化は「これはどのストアに存在するか」に答え、パスポリシーは「1 つのストア内で何ができるか」に答えます。レイヤーのアダプターをラップしてさらに制限することも、レイヤー化されたアダプターをラップしてマージ済みのビューを制限することもできます。スタック内のすべてのレイヤーは、1 つの MemorySchema を共有することが前提です。

エージェントに許可する操作を絞り込む

併用を想定した、独立した2つの設定です。一方は操作手段を取り除き、ツールがモデルに提示されないようにします。もう一方は機能を取り除き、呼び出し方にかかわらずアダプターが呼び出しを拒否します。

ツールの許可リスト。 useResourcesCurator(glove, resources, { tools: { deny: ["remove", "move"] } }) を使うと、ノートの整理はできても削除や移動はできないキュレーターになります。{ tools: { allow: ["ls", "read", "grep"] } } は何もない状態から開始し、許可するものを指定します。セレクターは完全名("glove_resources_remove")または短縮名("remove")で解決されます。まず allow で絞り込み、その後 deny で除外します。いずれのツールにも一致しないセレクターがあると MemoryToolSelectionError がスローされます。そうしないと、deny エントリーのタイプミスによってツールが登録されたままになるためです。独自のサーフェスを構築する場合に使えるよう、selectTools をエクスポートしています。これはプロンプトサーフェスのみを制御するもので、アダプターの機能はすべて維持されます。

パススコープのアクセス ポリシー。 withResourceAccess(adapter, policy) は ResourceFsAdapter をラップし、すべての呼び出しをパスに基づいて検証します。

const resources = withResourceAccess(new InMemoryResourcesAdapter({ schema }), {
  default: "none",
  rules: [
    { path: "/research", access: "read", note: "curated by the research team" },
    { path: "/notes", access: "write" },
    { path: "/**/*.locked.md", access: "read" },
  ],
});
useResourcesCurator(glove, resources);

"write" は読み取りと変更が可能です。"read" はすべての変更を拒否し、ResourceAccessError(code: "access_denied")を返します。"none" は読み取りを拒否し、ls/grep/glob/search/links_for の結果から除外します。path は絶対ディレクトリのプレフィックス、またはグロブ(*/**/?。glove_resources_glob と同じ書式)です。ルールは default に対して最後に一致したルールが優先されます(設定しない場合は "write")。検証はツール一覧ではなくアダプターで行われるため、読み取り専用フォルダーへの書き込みは、どのツールから要求されても拒否されます。また、2つの設定は併用できます。

知っておかないと困る詳細は次のとおりです。複数パスの読み取りは失敗するのではなく、結果がフィルターされます(隠されたパスを明示的に指定した場合は拒否されます。exists は例外をスローせず false を返します)。許可されたサブツリーに至るまでのディレクトリは引き続き一覧表示できるため、許可リスト形式のポリシーでも移動先をたどれますが、たどれることは読み取りアクセスを意味しません。再帰的な remove やディレクトリの move は、保護対象のパスに到達する場合は拒否されます。ポリシーはすべてのリソースツールの説明に反映されます(describe: false はテキストを非表示にしますが、検証は無効にしません)。制限付きポリシーでは replaceLinkTarget が拒否されます(整合性の調整はラップせずに実行してください)。また、findFilesNeedingEmbedding/setEmbedding はフィルターを通さずに処理されます。埋め込みループを実行するのはエージェントではなくホストだからです。

ツール一覧
エンティティ(useMemoryReader/useMemoryCurator)
ツール用途
glove_memory_findクラスとフィルターでノードを検索します。ファジー検索は任意です
glove_memory_getIDと1ホップの近傍ノードを指定してノードを取得します
glove_memory_queryクエリ DSL を使って構造化クエリを実行します
glove_memory_add_node識別キーでノードを作成またはアップサートします (キュレーター)
glove_memory_update_nodeノードのプロパティをパッチします (キュレーター)
glove_memory_connectエッジを作成または更新します (キュレーター)
glove_memory_disconnectエッジを削除します (キュレーター)
glove_memory_merge_nodesあるノードを別のノードに統合します (キュレーター)
エピソード(useEpisodicReader/useEpisodicCurator)
ツール用途
glove_episodic_searchエピソードの内容を検索します。アダプターに応じて、埋め込みベースのセマンティック検索、またはプロセス内のファジー検索/語彙検索を行います (supportsSemanticSearch がある場合のみ)
glove_episodic_find種類、参加者、時間範囲、プロパティで構造化フィルターを適用します
glove_episodic_timelineエンティティまたは時間枠の時系列一覧を表示します
glove_episodic_record新しいエピソードを追加します (キュレーター)
glove_episodic_update既存のエピソードにパッチします (キュレーター)
glove_episodic_deleteエピソードを削除します (キュレーター)
リソース(useResourcesReader / useResourcesCurator)
ツール目的
glove_resources_lsディレクトリの内容を一覧表示
glove_resources_readファイルの本文を読み取る(行範囲の指定は任意)
glove_resources_stat単一パスのメタデータを取得
glove_resources_grepツリー全体を対象にテキスト/正規表現で検索
glove_resources_glob名前パターンでパスを検索
glove_resources_searchセマンティック検索(supportsSemanticSearch の場合のみ)
glove_resources_links_for逆引き:対象にリンクしているリソースを検索
glove_resources_writeファイルを作成または上書き(curator)
glove_resources_edit一意な部分文字列を置換(curator)
glove_resources_mkdir空のディレクトリを作成(curator)
glove_resources_move名前を変更、または移動(curator)
glove_resources_removeファイルまたはディレクトリを削除(curator)
glove_resources_set_metadata本文を書き換えずにメタデータを更新(curator)
コンテキスト(useContext)
ツール目的
glove_context_getセクションごとにエントリーを読み取る、またはすべて一覧表示
glove_context_set新しいエントリーを追加
glove_context_update既存のエントリーをその場で更新
glove_context_unsetエントリーを削除、またはセクション全体を消去
フォーム(useFormRunner / useFormReader)
ツール目的
glove_form_list登録済みフォームの名前と説明を取得(モジュールの読み込みなし)
glove_form_startインスタンスを開始(初期値の指定は任意)
glove_form_status全体の中で現在開いているステップを取得(tier 1)
glove_form_inspect任意のステップ、単一フィールド、またはアウトライン全体を取得(tier 2)
glove_form_fill複数のフィールド ID をまとめて更新し、再評価された状態を返す
glove_form_reviseaction:set / retract / undo / redo
glove_form_abandon理由を付けて終了
glove_form_history過去の入力を読み取る(reader 登録)
MemorySchema — 共有オントロジー

すべてのアダプターに同じスキーマオブジェクトが渡されます。コード内に存在し、このパッケージはこれを永続化せず、デプロイ間で検証せず、移行用のプリミティブも公開しません。これは利用側の責任です。

import { MemorySchema } from "glove-memory/core";
import { z } from "zod";

const schema = new MemorySchema()
  .defineNodeClass({
    name: "Person",
    schema: z.object({ name: z.string(), email: z.string().optional() }),
    identityKeys: [["email"], ["name"]],          // multi-set: any matching set folds the write
    searchableProperties: ["name", "email"],      // indexed for fuzzy / contains
  })
  .defineRelationship({
    type: "worksAt",
    from: "Person",
    to: "Organization",
    propertiesSchema: z.object({ since: z.string().optional() }).optional(),
    multi: false,                                  // default — re-connect updates rather than duplicating
  })
  .defineEpisodeKind({
    name: "meeting",
    description: "A scheduled gathering.",
    propertiesSchema: z.object({ duration_min: z.number() }).optional(),
  })
  .defineResourceRoot({
    path: "/research",
    description: "External research artifacts.",
    semanticSearch: true,                          // default true; false skips embedding lifecycle for this root
  });

実行時に安全な変更:

  • 新しいノードクラス、リレーションシップ、エピソード種別の追加は、常に安全です。
  • 任意のプロパティの追加は、常に安全です。
  • 必須のプロパティを追加しても読み取りには影響しませんが、その値を指定しない新しい書き込みは検証に失敗します。
  • プロパティの削除や名前の変更には、利用側で管理する書き換えが必要です。アダプターは変更を検知しません。
  • ID キーを変更すると、後続の書き込みでノードが暗黙のうちに統合されたり、分割されたりする場合があります。
来歴 — 必須、追記のみ、すべての書き込みで適用

アダプターへの書き込みにはすべて Provenance が必要です。ノード、エッジ、エピソード、リソース、コンテキストエントリーごとに追記のみで記録されます。リーダー向けツールは結果から provenance を除外し、直接アダプターを呼び出した場合のみこれを返します。

interface Provenance {
  source: string;     // "conversation:<id>/turn:<n>", "manual", "import:<kind>:<id>"
  actor: string;      // "curator-run-xyz", "user:don", "system"
  timestamp: string;  // ISO 8601
  note?: string;      // free-form rationale (identity-merge decisions, conflict notes)
}

Link は共有の相互参照用語です。エピソードから人物へ、リソースからエピソードへ、コンテキストエントリーからプロジェクトへリンクします。

interface Link {
  kind: "entity" | "episode" | "resource";
  id: string;             // entity / episode id, or resource path
  relation?: string;      // free-form, e.g. "primary-contact", "source-transcript"
}

このパッケージは、リンク先が存在することを検証しません。アダプターは疎結合のままです。相互検証はキュレーター/オーケストレーターの役割です。

埋め込みのライフサイクル — アウトオブバンド、アダプター持ち込み方式

エピソードとリソースは同じライフサイクルを使用します。書き込み時にレコードへ embeddingStatus: "missing"(初期状態)または "stale"(コンテンツ変更時)を設定し、すぐに戻ります。別のプロセス(通常は Station のシグナル)が埋め込み処理を行います。

interface EmbeddingAdapter {
  dimensions: number;
  embed(texts: string[]): Promise<number[][]>;
}

更新ループ:

const pending = await episodic.findEpisodesNeedingEmbedding({ limit: 50 });
const vectors = await embedder.embed(pending.map((p) => p.content));
for (let i = 0; i < pending.length; i++) {
  await episodic.setEmbedding(pending[i].id, vectors[i]);
}

リソースでは findFilesNeedingEmbedding / setEmbedding を使用します(どちらも ResourceFsAdapter で任意であり、supportsSemanticSearch === true の場合にのみ存在します)。インメモリーアダプターでエピソードの古さを示すのはコンテンツの変更のみです。kind / participants / properties / occurredAt のパッチでは再埋め込みは行われません。異なる動作を求める利用者は、削除してから再記録できます。searchEpisodes の新しさのブレンドは、デフォルトで recencyWeight = 0.2、半減期30日です。

バックグラウンドインデックスを持つカスタムエピソードアダプター(検索機能の持ち込み)

embeddingStatus + findEpisodesNeedingEmbedding + setEmbedding のライフサイクルは、埋め込み専用ではなく、汎用的なバックグラウンドインデックス作成の接続点です。インデックスには、ベクトル、SQLite FTS5、Postgres tsvector、BM25、Meilisearch、Tantivy などを使用できます。カスタムインデックスで glove_episodic_search を実装するには、supportsSemanticSearch: true と searchEpisodes メソッドを備えた EpisodicMemoryAdapter を実装します。このメソッド群はそれぞれ異なる役割を担います。

  • 書き込み(recordEpisode / updateEpisode / deleteEpisode)は、プライマリストアに保存し、行に missing(新規)/ stale(コンテンツ変更時)を設定して、すぐに戻ります。ホットパスではインデックス作成を行いません。
  • 構造化読み取り(findEpisodes / episodesForEntity / episodesBetween)は、プライマリストアを直接参照するため、常に最新です。インデックスには触れません。
  • インデックスのライフサイクル(findEpisodesNeedingEmbedding → 成果物を構築 → setEmbedding)は、バックグラウンドワーカーのキューとコミットを担います。setEmbedding(id, vector) はインデックス成果物をコミットし、行の状態を fresh に切り替えます。
  • searchEpisodes(query, opts) はツールが呼び出すメソッドです。インデックスを検索し、opts.filter(kind / participantIds / timeRange)を適用して、EpisodeSearchResult[]({ episode, score, distance })を score の降順で返します。
// Out-of-band worker (Station signal / cron / queue). Index type is your choice.
const pending = await adapter.findEpisodesNeedingEmbedding({ limit: 100 });
const artifacts = await buildIndex(pending.map((p) => p.content)); // vectors | FTS docs | BM25 postings
for (let i = 0; i < pending.length; i++) await adapter.setEmbedding(pending[i].id, artifacts[i]);

契約上のルール(参照アダプターに合わせてください):supportsSemanticSearch: true が検索ツールを組み込むスイッチです。これがなければ searchEpisodes は使用されません。書き込んだばかりのエピソードは、ワーカーが追いついた後にのみ検索できます(結果整合性。find / timeline ではすぐに参照できます)。searchEpisodes は opts.filter を尊重し、score の降順で並べ替え、provenance を除外する必要があります。BM25のスコアは上限がないため、新しさのブレンド((1 - recencyWeight) * relevance + recencyWeight * recencyScore、デフォルトの recencyWeight = 0.2、半減期30日)の前に関連度を [0,1] に正規化してください。再フラグ付けはコンテンツ変更時のみ行ってください。setEmbedding の vector パラメーターはベクトルインデックスでのみ意味を持ちます。語彙ベースまたは外部インデックスの場合は無視し、setEmbedding を「ドキュメントを書き込み、fresh とマークする」処理として扱ってください(インターフェース上は引き続き必須です)。

調整用プリミティブ

このパッケージの契約は、意図的に限定されています。保存、クエリ、書き込み、検索です。アダプターをまたいだ連鎖処理は行いません。エンティティが統合または削除されても、古いIDを参照するエピソードは自動更新されません。オーケストレーターは次のプリミティブを使用します。

アクションプリミティブ
エンティティの統合episodic.replaceParticipantId(oldId, newId, prov)、resources.replaceLinkTarget("entity", oldId, newId, prov)
エンティティの削除episodic.findEpisodes({ where: { participantIds: [id] } })、resources.linksFor("entity", id) の後、オーケストレーターが判断
リソースの移動resources.replaceLinkTarget("resource", fromPath, toPath, prov)
エピソードの削除resources.linksFor("episode", id) の後、オーケストレーターが判断
古くなった埋め込みfindEpisodesNeedingEmbedding / findFilesNeedingEmbedding → embed → setEmbedding
参照用インメモリーアダプター

開発、テスト、簡易プロトタイプ向けです。すべて glove-memory/in-memory からエクスポートされ(バレルからも再エクスポートされます)。

import {
  InMemoryEntityAdapter,
  InMemoryEpisodicAdapter,
  InMemoryResourcesAdapter,
  InMemoryContextAdapter,
} from "glove-memory";

const entity = new InMemoryEntityAdapter({ schema });
const episodic = new InMemoryEpisodicAdapter({ schema, embedder });   // omit embedder → supportsSemanticSearch = false
// Embedding-free content search: `{ schema, fuzzySearch: true }` runs in-process
// lexical/fuzzy search over episode content — no embedder, no external service,
// supportsSemanticSearch = true. `embedder` wins if both are supplied.
const resources = new InMemoryResourcesAdapter({ schema, embedder }); // ditto
const context = new InMemoryContextAdapter({ schema });

プロセス内でのみ動作するため、再起動するとデータは失われます。本番プロジェクトでは、付属パッケージまたは独自のアダプターに置き換えます。

Forms — 会話を通じた構造化コレクション

定義はコードです。Zod スキーマ、ゲートクロージャー、エグゼキューターを、型が通る単一のビルダーチェーンにまとめます。エージェントが定義を読むことはありません。読み取るのは、評価済みの状態を投影したものです。

import { z } from "zod";
import { defineForm } from "glove-memory/forms";

export const travelClaim = defineForm({
  id: "travel-claim",
  version: 1,
  name: "Travel reimbursement claim",
  description: "Claimant, trip, travel and approval details.",
  conduct: "Conversational — one or two questions at a time. Don't read the field list aloud.",
})
  .step("claimant", { title: "Claimant", preview: "name, staff id, email" }, (s) =>
    s
      .field("fullName", { schema: z.string().min(2), label: "Full name" })
      .field("email", { schema: z.string().email(), label: "Work email" }),
  )
  .step(
    "travel",
    { title: "Travel", preview: "mode, mileage or ticket", when: (v, s) => s.stepComplete("claimant") },
    (s) =>
      s
        .field("mode", { schema: z.enum(["car", "rail", "air"]), label: "Mode" })
        .field("mileage", {
          schema: z.number().int().min(1).optional(),
          label: "Miles driven",
          when: (v) => v.mode === "car",          // applicability, not ask-order
        }),
  )
  .checkpoint("policy-cap", {
    when: (v) => typeof v.total === "number" && v.total > 750,
    blocking: true,
    run: () => ({ fail: "Over the limit — needs Finance pre-approval." }),
  })
  .onComplete(async (ctx) => {
    await ctx.memory.upsertNode("Person", { name: ctx.values.fullName });
  })
  .build();

各 .field() は累積する値の型を広げます。そのため、下流のすべての呼び出し箇所で ctx.values.mode は列挙型のユニオンに絞り込まれ、ctx.values.mileage は number | undefined になります。

オプションかどうか、型は宣言せずに導出されます。 required オプションはありません。フィールドがオプションになるのは、そのスキーマが undefined を受け入れる場合に限られます。推論される値の型も同じ条件に基づいて構築されます。エージェントが読み取る type 文字列は、z.toJSONSchema とレンダラーから生成されます("email address"、"one of: car | rail | air"、"integer >= 1")。これらを組み合わせることで、フィールド型の語彙が不要になります。型のユニオンも、レジストリも、拡張すべきものもありません。

書き込みがゲートで制限されることはありません。 ロックも、ロックされたフィールドのエラーもありません。glove_form_fill は任意のフィールド ID のパッチを受け取り、それぞれを個別に検証するため、1 つの値が不正でもほかの値は拒否されません。まだ適用できないものは破棄せず、保留中のエントリーとして保持します。2 問目を尋ねられているときに 6 問目に答えたユーザーは、6 問目に回答したことになります。フィールド ID も柔軟に解決されます。full_name、Full name、fullName はすべて、コンパイル時のエイリアスインデックスによって同じフィールドに解決されます。それでも一致しない ID には did_you_mean が返されます。

シーケンスは、互いに無関係な 2 つの要素に分かれます。when は、現在の回答に照らしてフィールドに意味があるかを決め、ステップは何に向けて誘導するかを決めます。

失われるものはありません。 entries は、各フィールドをリビジョンの追記専用ログに対応付け、そのうちどれを有効にするかをカーソルで示します。修正は上書きではなく追記されます。取り消しも 1 つのリビジョンなので、retract/undo/redo は、回答を失うことのないログ上でカーソルを移動するだけの操作となり、どの操作も元に戻せます。

await runner.retract("ticketReference"); // withdraw, keeping the answer
await runner.undo();                     // last answer anywhere on the form
await runner.redo("mileage");            // or on one field
await runner.history("mileage");         // every answer ever given

エージェントは 4 つの操作すべてを 4 つの動詞ではなく、glove_form_revise の action(set/retract/undo/redo)から利用します。ツールスキーマはモデル呼び出しのたびに再送信され、エージェント評価では、このサーフェス全体のコンテキストコストの約 75% を占めることがわかりました。

エグゼキューターは、1 つのシグネチャーを共有する 4 つのポイント(field.onFill、step.onComplete、checkpoint.run、form.onComplete)にまとめられ、コミット後に実行されます。実行されるのは立ち上がりエッジのみで、発生ごとに冪等性キーが付与されます。1 回のコミット内での実行順は検証済みです。field → step → checkpoint → form。エグゼキューターは { patch }、{ fail }、{ jump }、または { complete } を返します。ctx.memory は、エンジンから提供される来歴情報とともに、ほかの 4 つのサブシステムへの橋渡しをします。

トリガーで会話を誘導します。 チェックポイントはトリガーです。値に対する条件であり、立ち上がりエッジで発火します。{ jump } を返すと、開いているステップを前にも後ろにも移動でき、すでに完了したステップにも戻れます。

.checkpoint("verify-identity", {
  when: (v) => v.claimValue > 10_000,
  run: () => [{ patch: { verificationRequired: true } }, { jump: "claimant" }],
})

エグゼキューターは 1 つの効果または配列を返せるため、ルーターは 1 回の発火で値を設定し、ステップを移動できます。後方へのジャンプは再訪です。ステップの回答は filled のまま、ask: true が付いて戻ります。Tier 0 には back at step 1/3 "Claimant" — go through it again と表示されます(フォームがほかの点では完了している場合も同様です)。この上書きは、そのステップへの次の書き込みで解除されるため、ジャンプは誘導するだけで固定はしません。存在しないステップへのジャンプは無視されます。

値だけでなく状態に基づいてルーティングします。 when と run はどちらも、型付きの値に加えて FormState(stepComplete(id)、checkpointFired(id)、complete)を受け取ります。これにより、ルーターは会話がどこまで進んだかに応じて分岐できます。

.checkpoint("route", {
  when: (v, s) => Boolean(v.kind) && s.stepComplete("triage"),
  run: (ctx) => ({ jump: ctx.values.kind === "complex" ? "complex" : "simple" }),
})

checkpointFired はゲートが確認したカウンターを読み取ります。そのため、チェックポイント自身の run 内で問い合わせると、実行中の発火ではなく、その前に発火していたかどうかが返されます。 コレクションの終了。 { terminate: reason } はフォームを停止します。対象となるのは、不適格、重複、または取り下げなど、単に未完了なのではなく処理を続けるべきではないケースです。これは既存の2つの動作とは異なります。fail は拒否を記録して処理を続行し、complete は成功を確定します。terminate は closedReason を設定してインスタンスを終了し、すべてのフィールドへの確認を停止し、それ以降の書き込みを拒否して、Tier 0 から除外します。また、同じコミットで完了が記録されても、terminate が優先されます。

3段階の遅延読み込み。 Tier 0 は、各イテレーションの前にモデル入力の末尾へ追加される一時的な1行です。ここには、開いているステップ、そのステップで未入力のフィールドのラベル、および残りの各ステップの1行の preview が含まれます。Tier 1(glove_form_status)では、開いているステップの全情報を取得します。Tier 2(glove_form_inspect)では、ほかのステップ、フィールド、またはアウトライン全体を取得します。フォームモジュールは開始されるまでインポートされないため、glove_form_list のコストは名前と説明の取得だけです。

[form: travel-claim] step 2/4 "Trip" · pending: Destination, Departure date
later: Travel (mode, mileage or ticket) · Approval (cost centre, manager)

接続方法:

const registry = new FormRegistry().register("travel-claim", {
  name: "Travel reimbursement claim",
  description: "Claimant, trip, travel and approval details.",
  load: () => import("./forms/travel-claim").then((m) => m.travelClaim),
});

const { runner } = useFormRunner(glove, new InMemoryFormAdapter({ schema }), {
  registry,
  subject: conversationId,
  memory: { entity, episodic, resources, context },
});

useFormRunner は7つのツールをまとめ、Tier 0 の注入のために processRequest をラップし、モデルを介さずにホストからインスタンスの開始やチェックポイントの解決を行えるよう、ランナーを返します。useFormReader を使うと、別のエージェントに過去の入力内容への読み取り専用アクセスを提供できます。

FormAdapter の実装 — 担当するのは保存と取得だけです。セマンティクスはすべてエンジンが担います。次の4つの不変条件があります。

  1. entries は追記のみで、置き換えません。 commitInstance が受け取るのは、フィールドごとの { append?, cursor? } であり、履歴の置き換えではありません。ログを上書きすると、設計上保持が保証されている回答が失われます。まったく同じセマンティクスを再利用できるよう、applyEntryCommit がエクスポートされています。
  2. version は比較してから設定します。 古い ifVersion は FormConflictError で拒否し、書き込みが反映されるたびに値を増やします。
  3. コミットは全か無かです — エントリー、出現回数のカウンター、ディスパッチログ、ステータスをまとめて処理します。
  4. 読み取りではスナップショットを返します。

それ以外はすべて実装者に委ねられます。ストレージエンジン、スキーマ、インデックス作成、保持ポリシー、アトミック性の実現方法、来歴の詳細度、マルチテナント対応、暗号化などです。各メソッドの詳細は FormAdapter のドキュメントコメントを参照してください。

知っておくべき運用上の注意点: 記録済みの実行失敗は再試行されません(少なくとも1回の実行が保証されるのは、結果が記録される前にクラッシュした場合です)。ブロッキングチェックポイントが停止すると、resolveCheckpoint が呼び出されるまでインスタンスは awaiting のままです。タイムアウトはありません。recordDispatch は CAS の対象範囲外で書き込みます。すべてのフィールドがオプションのステップは開始時点で完了となるため、onComplete は直ちに実行されます。complete 状態のインスタンスは修正のために引き続きアクセスできます。フォームを終了するのは abandon のみです。

対象外
  • トリガー、スケジューリング、またはパイプラインのオーケストレーション(Station の担当範囲です)。
  • キュレーションロジックそのもの(利用側が設定します)。
  • 埋め込みの生成 — 利用側は独自の EmbeddingAdapter を組み込みます。
  • スキーマの永続化または移行 — スキーマはコード内にのみ存在します。
  • エンティティのマージ、エピソードの削除、またはリソース名の変更に伴うアダプター間のカスケード処理 — これは照合作業であり、オーケストレーターの責任です。
  • コンテキストに対するユーザー側の書き込み経路 — アダプターは set / update / unset を提供します。UI / API / フォームなど、ユーザーが設定を編集する場所からこれらを直接呼び出します。
  • バイナリーリソース。Resources はテキストのみです。
  • . と .. のパス解決。すべてのリソースパスは絶対パスです。
クイックリファレンス — 各要素の場所
必要なものシンボル
オントロジーを定義するglove-memory/core の MemorySchema
書き込みに必要なメタデータglove-memory/core の Provenance
サブシステム間の相互参照glove-memory/core の Link
埋め込みの契約glove-memory/core の EmbeddingAdapter
エンティティの契約glove-memory/entity の EntityMemoryAdapter
エピソード記憶の契約glove-memory/episodic の EpisodicMemoryAdapter
リソースの契約glove-memory/resources の ResourceFsAdapter
コンテキストの契約glove-memory/context の ContextAdapter
フォームの契約glove-memory/forms の FormAdapter
フォームを定義するglove-memory/forms の defineForm
フォームレジストリ(モジュールの遅延読み込み)glove-memory/forms の FormRegistry
フォームエンジン(ホスト側)glove-memory/forms の FormRunner
アダプターで append+clamp を再利用するglove-memory/forms の applyEntryCommit
リーダー/キュレーター用ヘルパーglove-memory/tools の useMemoryReader / useMemoryCurator、useEpisodicReader / useEpisodicCurator、useResourcesReader / useResourcesCurator、useContext、useFormRunner / useFormReader
フォールド後のツール群を絞り込む任意の use* ヘルパーの { tools: { allow, deny } }、glove-memory/tools の selectTools、ToolSelection
パスごとにリソースへのアクセスを制限するglove-memory/resources の withResourceAccess、ResourceAccessControl、getResourceAccessControl、ResourceAccessPolicy
共有ストアとプライベートストアを統合して1つのビューにするglove-memory/layered の layerEntity / layerEpisodic / layerResources / layerContext、MemoryLayer
プロセス内アダプターを参照するglove-memory/in-memory の InMemoryEntityAdapter、InMemoryEpisodicAdapter、InMemoryResourcesAdapter、InMemoryContextAdapter、InMemoryFormAdapter
エラークラスglove-memory/core の MemoryError、MemoryNotFoundError、MemorySchemaError、MemoryQueryError、MemoryWriteError、MemoryToolSelectionError、MemoryLayerError、EpisodicMemoryError、ResourceFsError、ResourceAccessError、ContextError、FormError / FormConflictError / FormStaleError / FormBlockedError / FormDefinitionError

型シグネチャの詳細はapi-reference.md — glove-memoryを、スキーマ定義、サブエージェントに委任するリーダー、キュレーターの構成、コンテキストのフロー、フォーム定義の例はexamples.md — Memoryをご覧ください。

画像ワークフロー(glove-image)

エージェントによる画像生成です。手作りの generate_image(prompt) ツールでは1回の呼び出ししか扱えませんが、glove-image はワークフロー全体を扱います。プロンプトはパイプラインで 構築 され、被写体と設定は 永続化 され、既存の画像を 入力 として使え、結果には 系譜 が付与され、モデルの呼び出しごとに 使用量が計測 されます。

使用する場面
  • 同じ被写体、スタイル、設定で画像を繰り返し生成する場合(キャラクターアート、ストーリーボード、商品写真、ブランド素材など)。
  • プロンプトの構築を、テンプレート文字列ではなく、検査可能かつ組み合わせ可能にしたい場合。
  • ユーザーが画像を持ち込み、出力を参照画像として再利用する場合。
  • 画像生成に実際にかかった費用を把握する必要がある場合。

「画像を1枚作って」というツールで十分なら、通常の glove.fold(...) のほうがシンプルです。

mountImage — 標準のエントリーポイント
import {
  mountImage, InMemoryImageAssetStore, InMemoryImageLibrary,
  expandCharacters, expandScenes, styleDirective, llmEnhance, UsageMeter,
} from "glove-image";
import { openrouterImages } from "glove-image/openrouter";

const meter = new UsageMeter();

await mountImage(glove, {
  adapter: openrouterImages(),             // ImageModelAdapter          (required)
  assets: new InMemoryImageAssetStore(),   // ImageAssetStore            (required)
  library: new InMemoryImageLibrary(),     // ImageLibraryAdapter        (required)
  model: createAdapter({ provider: "openrouter", model: "openai/gpt-4o-mini", stream: false }),
  pipeline: [expandCharacters(), expandScenes(), styleDirective("gouache"), llmEnhance()],
  review: { vision: visionAdapter, rounds: 1, rubric: "..." },  // opt-in vision
  usage: meter,
  onUsage: (source, usage) => billing.record(source, usage),
  curate: true,             // default — false folds read-only library tools
  candidates: 1,            // default; clamped to capabilities.maxCandidates
  requirePermission: false, // true gates generate/edit/regenerate
});

非同期で、チェーン不可です。build() の前後どちらからでも呼び出せます(mountMcp / mountMesh と同じ規約です)。パイプラインを検証し(エンハンサー名が重複しているとエラーになります)、fitToModel() を追加して、ツールをフォールドします。

プロンプトパイプライン(中核となる考え方)

生成では、モデルの生テキストが画像モデルに送信されることは決してありません。PromptDraft を作成し、エンハンサーを順番に適用して、各段階につき 1 つの TraceEntry を追加します。

interface PromptDraft {
  intent: string;        // the original ask — NEVER mutated
  positive: string;      // the working prompt
  negative?: string;
  refs: RefImage[];
  params: GenerationParams;                            // { size?, seed?, candidates?, extra? }
  requested: { characters: string[]; scene?: string }; // names from the call
  characters: CharacterDef[];  // resolved by expandCharacters()
  scene?: SceneDef;            // resolved by expandScenes()
  trace: TraceEntry[];         // { enhancer, note?, positive_after }
}

interface PromptEnhancer {
  name: string;   // must be unique across the pipeline
  run(draft: PromptDraft, ctx: EnhancerContext): Promise<PromptDraft | void>;  // void = keep mutated draft
}

interface EnhancerContext {
  library: ImageLibraryReader;
  assets: Pick<ImageAssetStore, "get" | "list">;
  model?: ModelAdapter;                  // the mount's `model` slot
  capabilities: ImageModelCapabilities;
  note(message: string): void;           // → this stage's trace entry
  recordUsage(usage: Partial<ImageUsage>): void;
  signal?: AbortSignal;
}
Inbetween動作
expandCharacters()名前が指定された各キャラクターの appearance をそのまま挿入し、negative を統合し、参照画像を identity 参照として添付します。名前が見つからない場合は、利用可能な名前を示す ImageCharacterNotFoundError を発生させます。
expandScenes()シーンの setting に対して同じ処理を行い、さらに style/composition 参照を追加します。
styleDirective(text)固定のハウススタイル句を追加します。
negativeDefaults(list)常設のネガティブ指定を、呼び出しごとの指定を重複させたり上書きしたりせずに統合します。
llmEnhance({ model?, instructions? })LLM による書き換えを 1 回行います。キャラクターの表現をそのまま保持するよう指示します。モデルがない場合は、トレースに注記を残してスキップします。トークン使用量を報告します。
fitToModel()必ず最後に追加される終端処理です。 モデルにネガティブ指定用のスロットがない場合は、negative を Avoid: … としてプロンプトに組み込みます。サポートされていない参照ロールを削除し、参照を maxRefs まで制限します(identity を優先)。size を最寄りの有効値に調整し、candidates を上限内に収め、サポートされていない seed を削除します。調整のたびにトレースへ注記を追加します。

渡されていない場合のデフォルトパイプラインは [expandCharacters(), expandScenes()] です。渡したパイプラインが中間処理を置き換えます。fitToModel() は、どの場合も最後に実行されます。

キャラクターとシーン
interface CharacterDef {
  name: string;              // kebab-case library key
  display_name?: string;
  appearance: string;        // ONE paragraph, spliced VERBATIM — write it prompt-ready
  notes?: string;            // non-visual; NEVER sent to the image model
  negative?: string;
  ref_images?: Array<{ asset: string; label?: string }>;  // identity anchors
  tags?: string[];
  created_at: string; updated_at: string;
}

interface SceneDef {
  name: string; display_name?: string;
  setting: string;           // location/era/palette/lighting/mood, spliced verbatim
  negative?: string;
  ref_images?: Array<{ asset: string; role: "style" | "composition"; label?: string }>;
  tags?: string[]; created_at: string; updated_at: string;
}

一貫性は記憶ではなく、繰り返しによって生まれます。表現はライブラリが管理し、呼び出しのたびに再挿入されます。良い生成結果をキャラクターの参照画像に昇格させる(ref_images: [{ asset: "img_..." }])ことが、「この見た目を固定する」操作です。

アセット、系譜、再生成
interface Recipe {
  kind: "generated" | "edited" | "assembled";
  intent?: string; finalPrompt?: string; negative?: string;
  params?: GenerationParams; adapter?: string;
  characters?: string[]; scene?: string;
  refs?: Array<{ asset: string; role: RefRole }>;
  trace?: TraceEntry[];
  parent?: string;        // "edited": source asset
  spec?: AssemblySpec;    // "assembled"
  usage?: ImageUsage;     // what this asset cost
}

glove_image_regenerate({ asset, tweak? }) は、generated のレシピを現在のパイプラインで再実行し、tweak を意図に追加します(ライブラリに加えられた変更も反映されます)。インポートまたは編集したアセットの再生成は、明確なエラーを返して拒否されます。

使用量とコストの追跡
interface ImageUsage { requests: number; tokens_in: number; tokens_out: number; cost_usd?: number }
type UsageSource = "generate" | "edit" | "enhance" | "review" | "describe";

スコープは 4 種類あります。呼び出し単位(data.usage)、アセット単位(Recipe.usage)、セッション単位(UsageMeter.report() → { total, by_source }。エージェントから glove_image_usage を通じて参照することもできます)、ホスト単位(onUsage(source, usage))です。アダプターは ImageModelResult で usage を返すことで使用量の報告に対応します。報告がないアダプターでも { requests: 1 } としてカウントされます。OpenRouter は実際の USD 額を報告します(usage: { include: true })。

ビジョン(オプトイン)

review: { vision, rounds?, rubric? } — ビジョン対応の ModelAdapter を利用できます。2 つの機能を提供します。glove_image_describe に visual_description が追加され、さらに rounds > 0 の場合は生成のたびに批評を行います(PASS → 完了、FAIL → 批評を修正メモとして追加し、review 段階としてトレースに記録して、もう 1 回実行します)。画像を 1 枚確認するたびに約 25k の入力トークンを消費するため、デフォルトでは無効です。

統合されたツール

glove_image_generate · _edit · _regenerate · _import · _describe · _asset_list · _assemble · _usage · _character_save/_get/_list/_remove · _scene_save/_get/_list/_remove(書き込みには curate が必要です)。

モデルに渡す data にはアセット ID、寸法、劣化情報、使用量が含まれます。renderData にはサムネイルのデータ URL が含まれます。バイト列が data に入ることはありません。そのため、セッションで扱う画像の枚数にかかわらず、コンテキストコストは一定です。

ImageModelAdapter(BYO)
interface ImageModelCapabilities {
  modes: Array<"generate" | "edit" | "variation">;
  maxRefs: number; refRoles: RefRole[];
  sizes: string[] | "flexible";
  negativePrompt: boolean; seed: boolean; maxCandidates: number;
}

interface ImageModelAdapter {
  name: string;
  capabilities: ImageModelCapabilities;
  generate(req: ImageGenerateRequest, signal?: AbortSignal): Promise<ImageModelResult>;
  edit?(req: ImageEditRequest, signal?: AbortSignal): Promise<ImageModelResult>;
}

マウント処理は呼び出し前に参照画像のバイト列を解決します。アダプターには実体化済みのバイト列が渡され、ストアに触れる必要はありません。また、リクエストがすでに対応範囲内であると見なせます(その処理は fitToModel が担います)。glove-image/openrouter から openrouterImages({ apiKey?, model?, baseUrl?, referer?, title? }) を提供しています(通常の fetch を使用し、デフォルトは google/gemini-2.5-flash-image。生成と編集、候補の並列展開に対応)。

クイックリファレンス — 各要素の場所
必要なものシンボル
サーフェスをマウントするmountImage(glove, { adapter, assets, library, … })
モデルの契約ImageModelAdapter, ImageModelCapabilities
参照モデルアダプターglove-image/openrouter の openrouterImages
アセットストレージの契約ImageAssetStore、参照実装は InMemoryImageAssetStore
キャラクター/シーンのストレージImageLibraryAdapter、参照実装は InMemoryImageLibrary
インビトウィーンを作成するglove-image/pipeline の PromptEnhancer
組み込みのインビトウィーンexpandCharacters, expandScenes, styleDirective, negativeDefaults, llmEnhance, fitToModel
パイプラインを手動で実行するcreateDraft, runPipeline, defaultPipeline
使用量の集計ImageUsage, UsageMeter, addUsage, emptyUsage
系譜ImageAsset.recipe の Recipe
アセンブリAssemblySpec(オプションのピア依存関係 sharp が必要)
バイトヘルパーsniffImage, toDataUrl, fromDataUrl
エラークラスImageError, ImageAssetNotFoundError, ImageCharacterNotFoundError, ImageSceneNotFoundError, ImageCapabilityError

メッシュネットワーク(glove-mesh)

glove-mesh を使うと、複数の Glove エージェントが、既存の glove-inbox プリミティブ上で、ダイレクトメッセージ、ブロードキャスト、確認応答をやり取りできます。このパッケージは、ランタイム API に小さな追加が 1 つある点を除き、動作上 glove-core に機能を追加するものです。追加されるのは IGloveRunnable.store(mountMesh が、解決済みの inbox アイテムを直接書き込むために読み取る readonly アクセサー)です。エージェントループのセマンティクスに変更はありません。このパッケージ自体には認証機能はなく、トランスポートと署名/検証は利用側の MeshAdapter が担います。

使用する場面
  • 同じプロセス内(またはプロセス/ホストをまたいで)非同期または並列で動作する 2 つ以上の Glove エージェントが、連携する必要がある場合。
  • カスタムのサブスクライバー/ストア統合を実装せずに、モデルが読めるプレーンなコンテキストとして、エージェントループにピアのメッセージを表示したい場合。

1 つのエージェントから、独立したサブタスクとして別のエージェントに作業を委任するだけであれば、代わりに defineSubAgent を使用してください。サブエージェントは親の配下でネストして実行されます。Mesh はピア間の連携に使用します。

メンタルモデル

各エージェントは、独自の StoreAdapter と inbox を持ちます。MeshAdapter はネットワークに対するエージェントごとのビューであり、McpAdapter の会話ごとのパターンに対応しています。エージェント A が glove_mesh_send_message({ to: "b", content: ... }) を呼び出すと、フレームワークは status: "resolved" の InboxItem を、タグ mesh:from:a とともに B のストアに追加します。B では、既存の Agent.injectResolvedInboxItems パス(glove-core)によって、次の ask() 時に合成ユーザーメッセージとして表示されます。これは、外部で解決された inbox アイテムとまったく同じです。

「共有 inbox」は概念上のものです。共有されるのはメッシュであり、inbox はエージェントごとに維持されます。

mountMesh — 標準のエントリーポイント

new Glove(...).build() の後に実行します。

import { mountMesh, MeshNetwork, InMemoryMeshAdapter } from "glove-mesh";

const network = new MeshNetwork();          // in-process bus; for distributed, BYO

await mountMesh(glove, {
  adapter: new InMemoryMeshAdapter(network, "agent-a"),
  identity: {
    id: "agent-a",
    name: "Agent A",
    description: "Plans tasks for the team.",
    capabilities: ["chat", "planning"],
  },
});

実行内容:

  1. glove.store が 4 つの inbox メソッド(getInboxItems, addInboxItem, updateInboxItem, getResolvedInboxItems)を実装していることを検証します。実装していない場合は MeshStoreUnsupportedError をスローします。
  2. adapter.register(identity) を待機し、ピアがこのエージェントを検出できるようにします。
  3. adapter.subscribe(handler) を 1 回呼び出します。ハンドラーは、受信メッセージを解決済み inbox アイテムに変換します(または、確認応答時に保留中のブロッキングアイテムを解決します)。
  4. 実行中の Glove に 4 つのツールを追加します。glove_mesh_send_message、glove_mesh_broadcast、glove_mesh_list_agents、glove_mesh_acknowledge です。

戻り値は Promise<void> で、チェーン可能ではありません。非同期セットアップの規約は mountMcp と同じです。

4 つのツール
ツール入力動作
glove_mesh_send_message{ to, content, in_reply_to?, blocking? }adapter.send を呼び出します。ブロッキングの場合、mesh:waiting:<msg_id> タグ付きの保留中ブロッキング受信トレイ項目を挿入し、確認応答または返信を受信すると解決します。
glove_mesh_broadcast{ content, blocking? }adapter.broadcast を呼び出します。ブロッキングの場合、最初に受信した確認応答で解決します。
glove_mesh_list_agents{ filter?: { capability?, name_contains? } }adapter.listAgents を呼び出し、フィルタリングして、自分自身を除外します。
glove_mesh_acknowledge{ message_id, note? }adapter.acknowledge を呼び出します。簡易的な確認にはこちらを使用し、内容のある返信には代わりに in_reply_to を指定した glove_mesh_send_message を使用してください。
MeshAdapter のコントラクト

エージェントごとに 1 つ実装します。コンシューマーが提供するもので、このパッケージがトランスポートを把握することはありません。

interface MeshAdapter {
  identifier: string;

  // Identity / registration
  register(identity: AgentIdentity): Promise<void>;
  unregister(): Promise<void>;
  listAgents(): Promise<AgentIdentity[]>;
  getAgent(id: string): Promise<AgentIdentity | null>;

  // Outbound
  send(message: MeshMessage): Promise<void>;
  broadcast(message: Omit<MeshMessage, "to">): Promise<void>;
  acknowledge(messageId: string, note?: string): Promise<void>;

  // Inbound — framework registers ONE handler per agent
  subscribe(handler: (msg: IncomingMeshMessage) => Promise<void>): () => void;
}

フレームワークが依存するアダプターの保証事項は次のとおりです。

  • send は、受信者がメッセージを処理したときではなく、トランスポートがメッセージを受け付けたときに解決します。
  • broadcast は、ファンアウト先から送信者を除外します。
  • ハンドラーのエラーを上位へ伝播させてはいけません。他のエージェントへのファンアウトを維持するため、ログに記録して処理を続けてください。
  • acknowledge は、kind: "ack" を持つ IncomingMeshMessage を、messageId の元の送信者へルーティングします。
リファレンス実装 InMemoryMeshAdapter

開発、テスト、単一ホストの構成で使用します。MeshNetwork は共有バスです。プロセス内で一度だけ構築し、同じインスタンスをすべての InMemoryMeshAdapter に渡してください。

import { MeshNetwork, InMemoryMeshAdapter } from "glove-mesh";

const net = new MeshNetwork();
const a = new InMemoryMeshAdapter(net, "agent-a");
const b = new InMemoryMeshAdapter(net, "agent-b");

MeshNetwork は、message_id → sender_id の有界 LRU(デフォルトは 1024 件)を保持します。これにより、確認応答のたびにモデルから送信者を渡さなくても、acknowledge が送信者へルーティングできます。

ブロッキング送信
ツール呼び出し保留中の項目?解決条件
glove_mesh_send_message({ blocking: false })いいえ該当なし
glove_mesh_send_message({ blocking: true })はい — mesh:waiting:<msg_id> タグ付きack_of === msg_id の確認応答、または返信(in_reply_to === msg_id を指定した glove_mesh_send_message)
glove_mesh_broadcast({ blocking: true })はい任意のピアから最初に受信した確認応答
glove_mesh_acknowledge(このエージェントが受信メッセージに確認応答する場合)いいえ該当なし — それ自体

保留中のブロッキング項目は、解決されるまで各ターンに Agent.buildPendingBlockingMessage を通じて一時的なリマインダーを生成します。確認応答または返信を受信すると、解決済みの項目が標準の [Inbox: N item(s) resolved] インジェクションを通じて表示されます。

返信は確認応答を兼ねます。 in_reply_to: X を含む直接受信メッセージは、返信本文を含む新しい解決済み受信トレイ項目を挿入すると同時に、X の保留中項目も解決します。これにより、受信者のツール呼び出しを 1 回省けます。

タグの規則

メッシュ項目では名前空間付きタグを使用するため、コンシューマーは受信トレイの履歴からメッシュの通信をフィルタリングできます。

タグのプレフィックス方向意味
mesh:from:<sender>受信直接メッセージ
mesh:broadcast:from:<sender>受信ブロードキャスト
mesh:waiting:<msg_id>ローカル送信したメッセージの保留中ブロッキング項目
認証モデル — ありません

MeshMessage.from は送信者が申告する値であり、検証されません。認証済みメッセージングが必要な場合は、send/broadcast を呼び出す前にメッセージに署名し、subscribe ハンドラーで検証してください。これは、認証をコンシューマー側の関心事として扱う McpAdapter.getAccessToken と同じ考え方です。

クイックリファレンス — 各機能の場所
必要な機能シンボル
エージェントにメッシュをマウントするglove-mesh の mountMesh(glove, { adapter, identity })
アダプターのコントラクトglove-mesh/core の MeshAdapter
メッセージの型glove-mesh/core の AgentIdentity、MeshMessage、IncomingMeshMessage
プロセス内バスglove-mesh/in-memory の MeshNetwork、InMemoryMeshAdapter
個別のツールビルダーglove-mesh/tools の buildMeshSendTool、buildMeshBroadcastTool、buildMeshListAgentsTool、buildMeshAcknowledgeTool
エラークラスglove-mesh/core の MeshError、MeshNotRegisteredError、MeshUnknownAgentError、MeshUnknownMessageError、MeshStoreUnsupportedError
glove_post_to_inbox との違い
  • glove_post_to_inbox —「会話の外から、後で自分で解決します」(外部サービス、Webhook、cron)。
  • glove_mesh_send_message —「メッシュ上の別の Glove エージェントと話しています」(ピアツーピア)。

どちらも同じ StoreAdapter の受信トレイに書き込みます。タグのプレフィックスで区別できます。

制限事項(v1)
  • InMemoryMeshAdapter はプロセス内でのみ動作し、再起動すると状態が消去されます。実際のトランスポートは利用者側で用意してください。
  • 送信者テーブルの LRU は 1024 件に制限されています。そのため、非常に古いメッセージへの確認応答はベストエフォートです。
  • ブロードキャストのブロッキングは、すべてのピアではなく、最初の確認応答を受け取った時点で解決します。
  • 新しい SubscriberEvent の種類はありません。可観測性は、4 つのツールの tool_use_result と受信トレイ状態への書き込みによって得られます。
  • グループ/トピックの概念はありません。ブロードキャストは、登録済みのすべてのエージェントを対象とします。

Continuum(glove-continuum-signal)

サブプロセスベースのランタイム基盤で、station-signal がバックグラウンドジョブを監視するのと同様に、Glove エージェントを監視します。実行モードは 2 つあります。

  • トリガー型(非同期) — エージェントはデフォルトで停止状態です。外部からの起動(.trigger(input)、スケジュールの実行、メッシュ経由の受信メッセージ)によってウェイクアップします。永続ストアを再開し、1 ターン実行してから戻り、停止状態になります。ウェイクアップのたびに新しいサブプロセスが起動します。
  • 並行型(同期) — エージェントは長時間稼働するサブプロセス内でウォーム状態を保ちます。ランナーはエージェントを稼働状態に維持し、runner.notify(name, input) を通じて通知をインラインで送信します。ループ途中でも即座に受け取るため、起動の遅延はありません。

この基盤はエージェント間プロトコルではありません。それは glove-mesh の役割です。Continuum はメッシュに、エージェントごとの安定した ID、受信トレイに対応する永続ストア、ウォーム状態のエージェント向けの長時間稼働するサブプロセスを提供します。メッシュは、利用者の MeshAdapter が提供するトランスポートを使い、そのサブプロセス内だけで動作します。

使用する場面
  • 1 つのデプロイメントで複数の長時間稼働エージェントを実行し、それぞれを分離したサブプロセスで動かしながら、中央から監視する場合。
  • 多数のウェイクアップをまたいで状態を維持するエージェントを使う場合(トリガー型エージェントのコンテキスト継続性)。
  • HTTP ハンドラー/cron/Webhook からエージェントの処理を起動し、非同期で実行する場合。バックグラウンドジョブのように扱えますが、ジョブは Glove エージェント全体です。
  • 外部ブローカーを使わず、同じホスト上のエージェント間でメッシュを構成する場合(サンプルの FilesystemMeshAdapter と組み合わせてください)。

Next.js ハンドラー内の単一プロセスでエージェントを実行する場合、continuum は不要です。引き続き createChatHandler を使用してください。

agent() ビルダー — モードを分岐として扱う
import { agent, z } from "glove-continuum-signal";
import { Glove, Displaymanager } from "glove-core";
import { createAdapter } from "glove-core/models/providers";

export const pizzaBaker = agent("pizza-baker")
  .input(z.object({ orderId: z.string() }))
  .output(z.object({ ready: z.boolean() }))
  .triggered()                                                // forks into TriggeredAgentBuilder
  .timeout(60_000)
  .retries(2)
  .every("5m").withInput({ orderId: "tick" })
  .env({ OVEN: "hot" })
  .store((name) => new MyPersistentStore(`./agents/${name}.db`))
  .onComplete(async (out, in_) => audit(out, in_))
  .factory(async (ctx) =>
    new Glove({
      store: ctx.store ?? undefined,
      model: createAdapter({ provider: "anthropic" }),
      displayManager: new Displaymanager(),
      systemPrompt: "You bake pizzas.",
      compaction_config: { compaction_instructions: "..." },
    })
      .fold(checkOrderTool)
      .build(ctx.store ?? undefined),
  );

// Fire-and-forget. Returns a run id immediately.
const runId = await pizzaBaker.trigger({ orderId: "abc-123" });

.triggered() は TriggeredAgentBuilder<TInput> を返します。.retries()/.every()/.withInput() はこの場合にのみ使用できます。.concurrent() は ConcurrentAgentBuilder<TInput> を返します。このビルダーから作成されたエージェントには、.trigger(input) に加えて .notify(input) インスタンスメソッドが用意されます(どちらも kind: "notify" の実行をキューに追加し、ウォーム状態のサブプロセスにルーティングします。ピアがウォーム状態であることが確実な場合は、より明確な名前の notify() を使ってください)。トリガー型エージェントで .notify() を呼び出すと、実行時エラーではなく型エラーになります。

ファクトリーコンテキスト(AgentFactoryContext):

  • name — 登録されたエージェント名
  • runId — トリガー型ではウェイクアップごとの ID、並行型のファクトリー設定中は "warmup"
  • mode — "triggered" | "concurrent"
  • store — .store(factory) からランタイムが作成した StoreAdapter(.store(...) が呼び出されていない場合は null)
  • subscriber — IPC 転送用の SubscriberAdapter。ファクトリーの戻り値を受け取った後、ブートストラップが防御的に再アタッチします。
  • controls.emit({ type, data }) — カスタムイベントを agent:event エンベロープでラップして、ランナーのサブスクライバーに送信します。
  • controls.signal — グレースフル停止/再起動/最終的な失敗時に発火する AbortSignal。メッシュのアンマウントや DB プールのクローズなどに使用します。
ContinuumRunner
import {
  ContinuumRunner,
  MemoryAdapter,
  ConsoleSubscriber,
} from "glove-continuum-signal";

const runner = new ContinuumRunner({
  agentsDir: "./agents",                  // auto-discover branded agents (recursive)
  adapter: new MemoryAdapter(),           // or your own ContinuumAdapter
  subscribers: [new ConsoleSubscriber()],
  pollIntervalMs: 1_000,
  maxConcurrent: 5,                       // triggered-run budget
  warmRestartPolicy: { maxRestarts: 5, backoffMs: 1_000 },
});

await runner.start();

// Triggered: spawn-per-wakeup. Returns a run id immediately.
const runId = await pizzaBaker.trigger({ orderId: "abc-123" });
const final = await runner.waitForRun(runId);

// Concurrent: routes to the warm subprocess inline.
const notifyId = await runner.notify("pizza-watcher", { event: "oven_ready" });

await runner.stop({ graceful: true, timeoutMs: 10_000 });

runner.notify(name, input) は concurrentAgent.notify(input) に相当する、ランナーに紐づいたメソッドです。エージェントの参照はなくてもランナーの参照がある場合(たとえば、ランナーを保持するラッパー内)に便利です。

永続ストア

トリガーされたエージェントには、起動のたびにコンテキストがリセットされないよう、起動をまたいで維持される StoreAdapter が必要です。.store(name => …) で設定します。ランタイムは起動のたびにエージェント名を引数としてこれを呼び出し、生成されたストアを ctx.store 経由でファクトリーに渡します。.store(...) を指定していないトリガー対象のエージェントについては、検出時に警告が出ます。同時実行エージェントでは、サブプロセスが長時間稼働するため、通常はインメモリストアで問題ありません(ただし、ランナーが再起動する可能性がある場合は永続化が必要です)。

メッシュ統合

ファクトリー内でエージェントごとにメッシュをマウントします。

import { mountMesh } from "glove-mesh";
import { makeRedisMeshAdapter } from "./infra/mesh.js";

agent("pizza-watcher")
  .input(z.object({ event: z.string() }))
  .concurrent()
  .store((name) => new MyInboxCapableStore(`./agents/${name}.db`))
  .factory(async (ctx) => {
    const glove = new Glove({ store: ctx.store ?? undefined, /* ... */ }).build();
    await mountMesh(glove, {
      adapter: makeRedisMeshAdapter(ctx.name),
      identity: { id: ctx.name, name: ctx.name, description: "..." },
    });
    return glove;
  });

mountMesh を使用するには、ストアが受信トレイのメソッド(getInboxItems / addInboxItem / updateInboxItem / getResolvedInboxItems)を実装している必要があります。Glove のデフォルトの MemoryStore はこれらを実装していますが、カスタムストアでも実装が必要です。glove-mesh の InMemoryMeshAdapter は単一プロセス用です。サブプロセスをまたぐエージェント間の通信には、実際のアダプター(Redis、NATS、HTTP Webhook など)を選ぶか、glove-continuum-signal/tests/fixtures/fs-mesh-adapter.ts のサンプル FilesystemMeshAdapter を使用してください(アトミックな一時ファイル作成とリネームによる書き込み、約 100ms 間隔でのポーリングによる購読、プロセスをまたぐ acknowledge() のためのメッセージごとの送信者検索に対応)。

アダプターの契約(ContinuumAdapter)

station-signal の SignalQueueAdapter に対応していますが、3 つの違いがあります。すべての箇所で signalName ではなく agentName を使うこと、RunKind に "notify" が含まれること、ステップを省略することです(Glove ではターンが処理単位です。細粒度の可観測性は、リレーショナルな Step 行ではなく、転送されるサブスクライバーイベントストリームにあります)。デフォルトとして MemoryAdapter が同梱されています。SQLite/Postgres などを使う場合は、利用側でアダプターを用意してください。HttpTriggerAdapter は、configure({ endpoint, apiKey }) によるリモートからの .trigger() を提供します。

サブスクライバーモデル

ContinuumSubscriber は、ライフサイクルコールバック(onAgentDiscovered、onAgentSpawned、onAgentReady、onAgentTerminated、onAgentRestarted、onRunDispatched、onRunStarted、onRunCompleted、onRunFailed、onRunTimeout、onRunRetry、onRunCancelled、onRunSkipped、onRunRescheduled、onNotifyDelivered、onCompleteError、onLogOutput)に加えて、すべての子サブプロセスからのすべての Glove SubscriberEvent を上流に転送する、単一の大きな onAgentEvent(envelope) を公開します。

interface AgentEventEnvelope<T extends SubscriberEvent["type"]> {
  agentName: string;
  runId: string | null;           // null for ambient warm-agent events between notifies
  mode: AgentMode;
  event_type: T;
  data: SubscriberEventDataMap[T];
  timestamp: string;
}

型ごとのコールバックではなく単一のエンベロープを使うのは、Glove の SubscriberEvent のユニオンが 14 種類以上あり、独立して増えていくためです。ラッパーであれば、型ごとの振り分けは簡単に実装できます。

信頼モデル
  • 登録済みエージェントのファイルは、検出時に await import() で読み込まれ、親の環境変数を引き継いでサブプロセス内で実行されます。agentsDir にユーザーの影響を受けるコンテンツを指定してはいけません。
  • 親プロセスの環境変数を転送する前に、NODE_OPTIONS、LD_PRELOAD、LD_LIBRARY_PATH、DYLD_INSERT_LIBRARIES は除外されます。また、エージェントの .env({...}) でこれらを上書きすることもできません。
  • ウォームサブプロセスでは、親が notify:* エンベロープの runId が送信元サブプロセスに属することを検証します(pendingNotifies による所有権チェック)。不正な動作をするウォームな子プロセスが、別のエージェントの実行完了を偽装することはできません。
  • ウォームサブプロセスには、名前ごとの再起動上限(warmRestartPolicy.maxRestarts、デフォルトは 5 回)が設定されます。この上限は ready 後に 60 秒間安定稼働するとリセットされるため、長時間稼働するデプロイでも、時折発生する短時間の停止によってウォームエージェントが恒久的に失われることはありません。ただし、クラッシュループでは上限に達し、再試行を停止します。
station-signal との違い
項目station-signalglove-continuum-signal
作業単位ステートレスなジョブ(ハンドラーの実行、またはステップ型パイプライン)ステートフルな Glove エージェントのターン
ビルダーの終端.run(fn) / .step().build().factory(ctx => Glove)
モード実行ごとにプロセスを起動する方式のみトリガー型(起動のたびにプロセスを起動)+並行型(ウォーム状態のサブプロセス+通知 IPC)
永続化アダプター内のステップ記録.store(name => …) を介して起動のたびに引き継がれる、エージェントごとの StoreAdapter
子プロセスはアダプターに書き込むか?はい(Step 行を書き込みます)いいえ(親プロセスのみが信頼できる情報源です)
可観測性タイプごとのサブスクライバーコールバックライフサイクルコールバック+転送された Glove イベント用の単一の包括的な onAgentEvent(envelope)
ユニット間メッセージング該当なしglove-mesh に委譲します(ファクトリー内でマウント)
制限事項(v1)
  • シングルランナーのみです。ウォームプールのマルチランナーシャーディングと、定期スケジュール向けの分散クレームリースは、将来のラッパーパッケージに委ねられています。
  • configure() はモジュールレベルのシングルトンです。同一プロセス内の複数のランナーで競合します。特定のランナーのアダプターを指定する必要がある場合は、runner.notify() を使用してください。
  • ウォーム状態のサブプロセスで通知が停止すると、タイムアウト時にその実行は失敗しますが、サブプロセスは終了しません。以降の通知はブートストラップのプロミスチェーンの後ろにキューイングされます。継続的な処理停滞が見られる場合は、ウォームエージェントを再起動してください。
  • 通知のキャンセルはベストエフォートです。親プロセスはステータスを cancelled に切り替えますが、ウォーム状態のサブプロセスではプロミスチェーンの実行が継続します。変更が重要な処理では、この点を考慮してください。
クイックリファレンス — 各機能の場所
用途シンボル
エージェントを定義するglove-continuum-signal の agent("name").input(zod).triggered() または .concurrent()、続けて .factory(ctx => glove)
エージェントを実行するglove-continuum-signal の new ContinuumRunner({ agentsDir, adapter, subscribers, ... })
ウォームエージェントに送信するrunner.notify(name, input) または concurrentAgent.notify(input)
永続化の契約glove-continuum-signal の ContinuumAdapter
デフォルトのインプロセスアダプターglove-continuum-signal の MemoryAdapter
リモートトリガーglove-continuum-signal の configure({ endpoint, apiKey }) + HttpTriggerAdapter
可観測性glove-continuum-signal の ContinuumSubscriber、ConsoleSubscriber、AgentEventEnvelope
ブランドシンボルglove-continuum-signal の AGENT_BRAND = Symbol.for("glove-continuum-agent")、isAgent(v)
ファクトリー内でメッシュをマウントするglove-mesh の mountMesh(glove, { adapter, identity })(メッシュのセクションを参照)

Glovebox — レガシーのサンドボックス型ランタイム(非推奨)

Glovebox は非推奨となり、Glove Foundry に置き換えられました。以下の詳細は、既存の Glovebox デプロイメントの保守や移行のためだけに記載しています。新しいエージェントアプリケーションで Glovebox を選択しないでください。新しいランタイム、ワークスペース、VFS、アーティファクトのワークフローには、glove-foundry スキルと Foundry 作業環境を使用してください。

レガシーの Glovebox は、ビルド済みの Glove エージェントを、独立したネットワークアクセス可能なサービスとしてパッケージ化します。開発者は Glove エージェントを作成し、glovebox.wrap(runnable, config) を呼び出してから glovebox build を実行すると、デプロイ可能なアーティファクト(Dockerfile + nixpacks.toml + esbuild サーバーバンドル + マニフェスト + 認証キー)が生成されます。デプロイされたサーバーは、セッションごとに認証付き WebSocket エンドポイントを1つ公開し、単一のソケット上で id によってプロンプトを多重化します。

使用する場面
  • ホストプロセスでは安全に(または移植性を保って)提供できないシステムツール(ffmpeg、pandoc、ヘッドレス Chromium、ImageMagick、qpdf、libreoffice)を必要とする Glove エージェントがあり、それらをデプロイ先ごとにインストールするのではなく、コンテナ内でサンドボックス化したい場合。
  • Glove エージェントのネットワーク経由で呼び出せるインターフェースを用意し、エージェントループを再実装することなく、React フロントエンド、Node サービス、その他のエージェントから利用したい場合。
  • 安定した入力/出力の契約が必要な場合。クライアントはプロンプトとファイルを送り、最終メッセージと出力ファイルを受け取ります。その間にどのアダプターやツールが動作したかは問いません。

チャット専用のエンドポイントには、glove-next の createChatHandler を使用してください。エージェントランタイム、動的な作業環境、永続的なインスタンス、複数のクライアントやエージェントには Foundry を使用してください。移行が現在のタスクの範囲外である場合に限り、Glovebox を引き続き使用してください。

3 つのパッケージ
  • glovebox — 作成機能と glovebox build CLI。packages/glovebox/src/index.ts に公開 API があります(glovebox.wrap、config、protocol、storage の再エクスポート)。ストレージ DSL(rule.inline、rule.url、rule.localServer、rule.s3、composite)。ワイヤープロトコルの型(FileRef、ClientMessage、ServerMessage、Manifest、StoragePolicyEncoded)。
  • glovebox-kit — コンテナ内ランタイム。packages/glovebox-kit/src/server.ts の startGlovebox(opts)。起動時に 2 つのスキル(environment、workspace)と 2 つのフック(/output、/clear-workspace)を自動で挿入し、buildEnvironmentBlock で構築した静的な環境ブロックをエージェント既存のシステムプロンプトの先頭に追加します。WS アップグレードエンドポイントと併せて、/health(公開)、/environment(Bearer 認証)、/files/:id(Bearer 認証)の HTTP ルートをホストします。
  • glovebox-client — クライアント SDK。GloveboxClient.make({ endpoints }) で名前付きエンドポイントを登録します。client.box(name) は遅延構築される Box を返します。box.prompt(text, { files }) は、非同期反復可能な events/display と、message/outputs の Promise、および read(name) ヘルパーを備えた PromptResult を返します。
ベースイメージ

5 つの事前構築済みベースイメージが docker/ にあり、ghcr.io/porkytheblack/glovebox/<name>:<tag> に公開されています(レジストリは GLOVEBOX_REGISTRY 環境変数で上書きできます)。タグは packages/glovebox/src/build/dockerfile.ts の KNOWN_BASE_TAGS で固定されています。

ベースタグ内容
glovebox/base1.0Node 20、glovebox ユーザー(uid 10001)、/work + /input + /output + /opt/glovebox-prebuilt/node_modules に事前構築済み better-sqlite3
glovebox/media1.4base + ffmpeg、imagemagick、sox、yt-dlp
glovebox/docs1.2base + pandoc、qpdf、pdftk-java、ghostscript、libreoffice headless
glovebox/python1.3base + uv、numpy、pandas、pillow、scipy、matplotlib
glovebox/browser1.1base + Chromium を含む Playwright

標準ベースでは、生成される Dockerfile のユーザー/レイアウト設定を省略し、事前構築済みの better-sqlite3 を ln -sfn でサーバーバンドルの node_modules にリンクします。カスタムベースでは、出力された package.json に対して通常の npm install --omit=dev を実行します。

作成:glovebox.wrap

いつもどおり Glove エージェントを構築し、GloveboxApp をデフォルトエクスポートとして出力します。

// glovebox.ts
import { glovebox, rule, composite } from "glovebox-core"
import { agent } from "./my-agent"   // your built IGloveRunnable

export default glovebox.wrap(agent, {
  name: "pdf-extractor",
  base: "glovebox/docs",
  packages: { apt: ["poppler-utils"] },
  storage: {
    inputs: composite([rule.url(), rule.inline()]),
    outputs: composite([
      rule.inline({ below: "1MB" }),
      rule.localServer({ ttl: "1h" }),
    ]),
  },
  env: {
    ANTHROPIC_API_KEY: { required: true, secret: true },
  },
  limits: { memory: "2GB", timeout: "10m" },
})

デフォルト値(packages/glovebox/src/config.ts より):

  • base:"glovebox/base"。
  • fs:{ work: "/work" (rw), input: "/input" (ro), output: "/output" (rw) } — DEFAULT_FS。
  • storage.inputs:DEFAULT_INPUTS_POLICY — 常に url を試し、失敗した場合は inline にフォールバックします。
  • storage.outputs:DEFAULT_OUTPUTS_POLICY — 1 MB 未満では inline、それ以外では TTL 1 時間の localServer を使用します。

ストレージルールの順序は重要です。composite([...]) は呼び出し元の順序を維持し、pickAdapter はルールを先頭から順に評価し、最初に一致したものを採用します。always: true はどこにあっても終端ルールになり、default: true は他のルールに一致しない場合に使用されるフォールバックです。validateOutputsPolicy は、読み取り専用の url アダプターを対象とする、または終端ルールを省略する出力ポリシーを拒否します。コンテナは起動時に即座に失敗します。

ビルド:glovebox build
pnpm add -D glovebox
npx glovebox build ./glovebox.ts --out ./dist --name pdf-extractor

dist/ 配下に次のものを出力します。

Dockerfile           # FROM <resolved base>, optional apt/pip/npm, COPY server, CMD ["node", "index.js"]
nixpacks.toml        # Same recipe for Railway / Render / nixpacks-aware platforms
glovebox.json        # The Manifest (name, version, base, fs, env, limits, key_fingerprint, storage_policy, packages, protocol_version: 1)
glovebox.key         # 32-byte hex random — the bearer token. KEEP SECRET. Re-runs reuse if present.
.env.example         # Generated from `env` config — required vars first
server/
├── index.js         # esbuild ESM bundle: glovebox-kit + glovebox + glove-core + your wrap module
├── package.json     # only declares "better-sqlite3" — the one native dep
└── glovebox.json    # Manifest copy (the runtime resolves it via `new URL("./glovebox.json", import.meta.url)`)

ビルドが出力する合成 ESM エントリは、環境変数から GLOVEBOX_KEY(必須)、GLOVEBOX_PORT(デフォルトは 8080)、GLOVEBOX_PUBLIC_URL(任意)を読み込み、wrap モジュールのデフォルトエクスポートを取得して、startGlovebox(...) を呼び出します。esbuild 呼び出しで external: に指定されるのは better-sqlite3 のみです(server-bundle.ts の NATIVE_EXTERNALS)。

ランタイムインジェクション

コンテナ内で、startGlovebox は起動時に一度だけ applyInjections(runnable, config, getExfilState) を実行します(packages/glovebox-kit/src/injection.ts を参照)。これにより、次のものが追加されます。

  • environment スキル — エージェントから呼び出し可能です。解決済みの設定(name、version、base、fs、packages、limits)を JSON として返します。エージェント自身による環境の確認に役立ちます。
  • workspace スキル — エージェントから呼び出し可能です。すべての fs マウント(work、input、output)の最新の内容を一覧表示します。
  • /output フック — parseTokens ディレクティブです。エージェントが /output /tmp/some/extra/file.png と書き込むと、その絶対パスがリクエストごとの extraOutputs: Set<string> に追加されます。ターンの完了後、このセットに含まれるものは、エージェントが /output に書き込んだものと一緒にアップロードされます。
  • /clear-workspace フック — /work マウントに対する rm -rf です。work が設定されていない場合は何もしません。

buildEnvironmentBlock(config) も起動時に既存のシステムプロンプトの先頭に一度だけ追加されるため、手動で設定しなくても、エージェントは /work、/input、/output、利用可能な apt パッケージ、制限を把握できます。リクエストごとの入力一覧は環境ブロックには含まれません。エージェントは必要に応じて workspace スキルを呼び出し、/input を読み取ります。

ワイヤープロトコルの構造

セッションごとに WebSocket が 1 つあり、アップグレード時に Authorization: Bearer <key> で認証されます。プロンプトは id によって多重化されます。v1 では、サーバーはセッション内のプロンプトを直列化します(server.ts の promptChain.then(...))。Glove の PromptMachine と Context は並行呼び出しに対応していません。この制限は v2 で解除する予定のため、プロトコルは多重化を前提とした構造になっています。

メッセージタイプは packages/glovebox/src/protocol.ts にあります。

  • クライアント → サーバー:prompt、abort、display_resolve、display_reject、ping。
  • サーバー → クライアント:event(SubscriberEvent を 1 対 1 で反映)、display_push / display_clear(attachDisplayBridge によって Glove のディスプレイスタックをワイヤーに接続)、complete(最終アシスタントメッセージと outputs マップ)、error、pong。

FileRef は、境界を越えて送受信されるファイルのワイヤー形式です。

type FileRef =
  | { kind: "inline"; name; mime; data }              // base64
  | { kind: "url"; name; mime?; url; headers? }
  | { kind: "server"; name; mime; size; id; url }     // /files/:id on the same server
  | { kind: "s3"; name; mime?; bucket; key; region? }
  | { kind: "gcs"; name; mime?; bucket; object }      // v2-deferred adapter

サーバーは pickAdapter(policy, { size }, registry) を使って各出力のアダプターを選択します。入力は、ref の kind に基づいて pickAdapterForRef(ref, registry) で読み取られます。サーバーのデフォルトには常に inline、url、localServer が含まれます。s3 は、adapters 経由でアダプターを登録した場合にのみ解決されます(以下を参照)。

認証モデル

Bearer のみで、デプロイごとに単一のキーを使用します。ビルド CLI は 32 バイトの 16 進数キーを dist/glovebox.key に書き込み、その SHA-256 フィンガープリントをマニフェストに保存します。起動時に verifyAgainstManifest が、設定された GLOVEBOX_KEY がマニフェストのフィンガープリントと一致すること、および提示された bearer が設定キーと一致することの両方を確認します。verifyBearer は timingSafeEqual を使って定数時間比較を行います。マニフェスト内のフィンガープリントから情報が漏れることはありません。検証には引き続き生のキーが必要です。

同じキーで WS アップグレード、/environment、/files/:id を保護します。JWT とセッションごとのトークンは V2 以降に対応予定です。

adapters エクスポートによるカスタムストレージアダプター

wrap モジュールでは、デフォルトエクスポートとともに adapters 関数(または値)をエクスポートできます。合成ビルドエントリはそれを await し、結果を startGlovebox({ adapters }) に渡します。アダプターは名前でデフォルトのレジストリに追加されるため、s3 はポリシーで指定できる実際のアダプターになります。

// glovebox.ts
import { glovebox, rule, composite } from "glovebox-core"
import { S3Storage } from "glovebox-kit"
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
import { agent } from "./my-agent"

const s3 = new S3Client({ region: process.env.AWS_REGION })

export const adapters = () => ({
  s3: new S3Storage({
    bucket: process.env.OUTPUTS_BUCKET!,
    region: process.env.AWS_REGION,
    uploadObject: async ({ bucket, key, body, contentType }) => {
      await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }))
    },
    downloadObject: async ({ bucket, key }) => {
      const out = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }))
      return new Uint8Array(await out.Body!.transformToByteArray())
    },
  }),
})

export default glovebox.wrap(agent, {
  base: "glovebox/media",
  storage: {
    outputs: composite([
      rule.inline({ below: "1MB" }),
      rule.s3({ bucket: process.env.OUTPUTS_BUCKET! }),
    ]),
  },
})

キットには「遅延」アダプターとして S3Storage が含まれています。ランタイムイメージに具体的な SDK 依存関係はありません。独自のコードベースから uploadObject/downloadObject のサンクを渡します。outputs ポリシーが s3 を対象にしているのにアダプターが登録されていない場合、キットは起動時にエラーをスローします(validateOutputsPolicy は、リクエストごとのオーバーライドを含む、すべての有効なポリシーに対して実行されます)。

デプロイ

アーティファクトはプラットフォームに依存しません。一般的な方法は次の 2 つです。

  • Docker — docker build -t my-app dist/ を実行してから、-p 8080:8080 -e GLOVEBOX_KEY=$(cat dist/glovebox.key) -e GLOVEBOX_PUBLIC_URL=https://my-app.example.com my-app を指定して実行します。必要な環境変数は .env.example から設定してください。
  • Railway/Render/nixpacks — dist/ をリポジトリにプッシュすると、プラットフォームが nixpacks.toml を検出します。プラットフォームの UI で GLOVEBOX_KEY と必要な環境変数を設定してください。server 種別の FileRefs にクライアントからアクセスできるよう、GLOVEBOX_PUBLIC_URL にはデプロイ先の公開 URL を指定してください。

GLOVEBOX_PORT のデフォルト値は 8080 です。Dockerfile ではこれを EXPOSE と ENV に設定します。GLOVEBOX_PUBLIC_URL のデフォルト値は http://localhost:<port> です。ローカルでは問題ありませんが、コンテナ外のクライアントには機能しません。

クライアントからの呼び出し
import { GloveboxClient } from "glovebox-client"

const client = GloveboxClient.make({
  endpoints: {
    pdf: { url: "wss://pdf.example.com/", key: process.env.GLOVEBOX_PDF_KEY! },
  },
})

const box = client.box("pdf")
const env = await box.environment()        // Bearer-auth'd GET /environment, cached after first call
console.log(env.base, env.packages.apt)

const result = box.prompt("extract tables from invoice.pdf", {
  files: { "invoice.pdf": { mime: "application/pdf", bytes: pdfBytes } },
})

for await (const event of result.events) {
  if (event.event_type === "text_delta") {
    process.stdout.write((event.data as { text: string }).text)
  }
}

const message = await result.message
const outputs = await result.outputs       // Record<string, FileRef>
const csvBytes = await result.read("tables.csv")  // routes via ClientStorage based on FileRef.kind

PromptResult は events と display の両方を非同期イテラブルとして公開します。表示イベントはリクエスト単位ではなくセッション単位のため、アクティブなすべてのプロンプトの表示ストリームに配信されます。result.resolve(slot_id, value)/result.reject(slot_id, error) は表示への応答を返し、result.abort() は { type: "abort", id } を上流に送信します。

DefaultClientStorage が対応するのは inline と url(および Bearer 認証 HTTP 経由の server)のみです。入力を先に S3 に保存する必要がある場合は、カスタムの ClientStorage を GloveboxClient.make({ storage }) に渡してください。

デバッグ
  • GET /health は認証不要で、{ ok: true, name, version } を返します。ライブネスプローブに便利です。
  • Authorization: Bearer <key> を指定した GET /environment は、マニフェストの name、version、base、fs、packages、limits、protocol_version を返します。クライアント SDK は初回呼び出し後にこれをキャッシュします。box.environment() が 401 を返す場合はキーが間違っています。予期しない base が返される場合は、古いマニフェストからデプロイがビルドされています。
  • マニフェストのフィンガープリント不一致 — Configured GLOVEBOX_KEY does not match the manifest fingerprint というエラーで起動に失敗します。キーを再生成してから再ビルドしていないか、誤ったキーのファイルを参照しています。
  • Outputs policy references unregistered adapter: s3 — wrap モジュールの adapters エクスポートに s3 エントリがなく、validateOutputsPolicy がポリシーを拒否しました。アダプターを追加するか、outputs から rule.s3(...) を削除してください。
  • 完了メッセージが空 — processRequest が、最終メッセージに text がない結果を返しました。エージェントの通常の完了処理を確認してください。キットがそのまま読み取るのは result.messages[last].text(または result.text)のみです。
  • プロンプトが停止したままになる — v1 では promptChain.then(...) を使ってセッションごとにプロンプトを直列化します。プロンプトが停止すると(たとえば pushAndWait のリゾルバー待ちなど)、同じセッションの後続プロンプトもすべて待機します。チェーンを解除できるよう、クライアントが display_resolve を送信するか、WS を閉じるようにしてください。
V2 まで延期された注意事項

v1 のキットとプロトコルは、意図的に対象を絞っています。以下の制限を考慮して計画してください。

  • 多重実行 — ワイヤ形式は多重化に対応していますが、サーバーは対応していません。並列実行を期待して、1 つのセッションで複数のプロンプトをパイプライン処理しないでください。
  • JWT 認証 — デプロイごとに共有のベアラキーを 1 つ使用します。ユーザーごとの JWT やスコープ付きトークンは v2 まで延期されています。
  • ホットリロード — ありません。wrap-config を変更するたびに、再ビルドして再デプロイしてください。
  • GCS / Azure アダプター — inline、url、localServer、および(adapters エクスポート経由の)s3 のみがサポートされています。gcs は FileRef に存在しますが、アダプターを登録しないと実行時にエラーになります。
  • ベースごとの事前登録済みサブエージェント — @transcoder(メディア)、@pdfwright(ドキュメント)、@analyst(Python)、@scraper(ブラウザー)への対応は v2 で追加されます。v1 でこれらが必要な場合は、自分で配線してください。

型シグネチャの詳細は api-reference.md — glovebox / glovebox-kit / glovebox-client を、実例は examples.md — Pattern: Glovebox PDF Extractor を参照してください。

Scratchpad(glove-scratchpad)

LLM のツール使用向けデータベースエミュレーターです。 数十個のツール定義をコンテキストウィンドウに読み込む代わりに、エージェントの機能を、単一の execute_sql ツールでクエリするリレーショナルデータベースとして公開します。モデルはあらゆる規模で SQL を流暢に扱えるため、クエリを記述して機能を発見、呼び出し、組み合わせることができます。リソースはテーブルになります。リソースはエンティティ/データ型(github_pr、linear_issue、emails、time、images)であり、その CRUD 操作は(異なる場合もある)基盤のツールに対応します。

本質的には SQL インタープリターです。すべてのステートメントは、ツールが実行される前に解析および検査されます。これにより、発見(information_schema)、組み合わせ(JOIN/INSERT … SELECT)、プレビュー(EXPLAIN、トランザクション)、そして実効性のあるセキュリティ境界(拒否可能な構文木)を、データベースがすでに何十年も前に解決しているため、追加コストなしで実現できます。

使用する場面
  • エージェントに多くの機能があり、すべてのツール定義を読み込むとコンテキストが肥大化する場合。execute_sql ツール 1 つでカタログ全体を置き換え、モデルは information_schema を通じて機能を発見できます。
  • 1 つのステートメントでサービス間の組み合わせを行いたい場合(INSERT INTO notion_page SELECT … FROM github_pr WHERE merged)。中間行をコンテキストに戻す必要はありません。
  • BEGIN … COMMIT を使って外部への作用(承認が必要な書き込み)を準備してプレビューしたい場合、または ROLLBACK でドライランしたい場合。
  • 大規模な MCP フリートを連携させ、統一されたクエリ可能な形式で扱いたい場合(glove-scratchpad/mcp)。

ファーストパーティーツールが少数しか必要ない場合は、通常の glove.fold(...) のほうがシンプルです。カタログが大きい場合や、組み合わせ/プレビューが必要な場合に Scratchpad が役立ちます。

クイックスタート
import { Database, resourceFromTool, defineResource, mountDatabase } from "glove-scratchpad";
import { z } from "zod";

const db = await Database.create({ policy: { writes: true } });   // writes off by default

// A read-only tool → a one-row `time` table. Columns come from a Zod schema.
db.register(resourceFromTool(getTimeTool, {
  name: "time", volatility: "stable",
  schema: z.object({ now: z.date(), tomorrow: z.string() }),
}));

// A search tool → a volatile `web` table; the tool's required `query` input
// becomes a required-key column pushed down through WHERE.
db.register(resourceFromTool(searchTool, {
  name: "web", volatility: "volatile",
  schema: z.object({ title: z.string(), url: z.string() }),
}));

mountDatabase(agent, { db });   // folds execute_sql (+ explain_sql) and primes the prompt

以降、モデルは SQL だけで作業できます。例:SELECT table_name FROM information_schema.tables、SELECT title, url FROM web WHERE query = 'sql engines' など。

テーブルとしてのリソース(Zod ファースト)

リソースは列と、独立して接続された CRUD 動詞の一部またはすべてを持つエンティティです。Zod の schema で定義すると、1 つのオブジェクトが列であり、エンドツーエンドの行型にもなります。スキーマはすべてのリゾルバーに渡されるため、select はその型の行を返し、insert はその型の行を受け取り、update の set は部分型になり、bindings.one("col") ではスキーマの列名が自動補完されます。列、キー、書き込みペイロードの誤りは、実行時に気付かれないバグではなくコンパイルエラーになります。

import { z } from "zod";

const githubPr = defineResource({
  name: "github_pr",
  volatility: "stable",
  schema: z.object({
    number: z.number().int().describe("PR number"),   // an API argument (see keys)
    title: z.string(),
    merged: z.boolean(),
  }),
  keys: ["number"],                                    // required WHERE-pushdown key(s), typed to the schema
  select: (b) => listPrs({ number: b.one("number") }), // SELECT → a list/get tool (b.one autocompletes columns)
  insert: (rows) => createPr(rows[0]),                 // INSERT → a create tool (rows typed to the schema)
  update: (set, b) => updatePr(b.one("number"), set),  // UPDATE → an update tool (set: Partial<row>)
  delete: (b) => closePr(b.one("number")),             // DELETE → a close tool
});

各動詞は任意で、それぞれ独立して接続できます。読み取り専用の time には select のみ、emails(送信)リソースには insert のみ、images 生成リソースには select の形をした揮発性の関数リレーションのみがあります(SELECT url FROM images WHERE prompt = '…' — prompt は引数です)。動詞の有無が機能のゲートになります。書き込み専用リソースを SELECT したり、書き込み機能のないリソースに書き込んだりすると、明確なエラーになります。必須キー列にはプッシュダウンされた WHERE の値が自動設定されるため、select でそれらを省略できます。

Zod → Postgres の型マッピング(単独でエクスポートされる columnsFromZod 経由):

ZodPostgres の列型
z.number().int(), z.bigint()bigint
z.number()double precision
z.boolean()boolean
z.string()text
z.date(), z.iso.datetime()timestamptz
z.object(...), z.array(...), 表現できない型jsonb(-> / ->> 経由でアクセス可能)
任意のフィールド .meta({ pgType: "…" })その正確な型(オーバーライドが優先)

フィールドの .describe(...) は列の説明になります。作者はそこに enum や許容値のヒント(status: unresolved | resolved | ignored)を記述し、モデルは information_schema とプライミング済みカタログを通じて読み取ります。スキーマを使うのが望ましいですが、Postgres の型を手作業で指定したい場合は、従来どおり columns: [{ name, type, requiredKey? }] の形式も使えます。

resourceFromTool(tool, spec) は、単一動詞のシンプルなケース(1 つのツールを 1 つの動詞を持つリソースに変換する場合)に便利です。ツール自身の入力スキーマ(必須の Zod フィールドまたは jsonSchema.required エントリー)から必須キー列を導出し、出力列には schema(または columns)を使います。op で動詞を選択します(デフォルトは "select")。揮発性 SELECT では列を宣言する必要があります(0 行を返す初回呼び出しから安定したスキーマを推論することはできません)。

揮発性

各リソースでは、Postgres の immutable | stable | volatile を宣言します。インタープリターは execute ごとに、揮発性に関係なく各リソースを必ず 1 回だけ解決します(そのため、効果を伴う読み取りがプランナーによって N 回呼び出されることはありません)。揮発性はさらに、キャッシュの動作を決定します。

  • immutable — データベースの存続期間中キャッシュされます(純粋なルックアップ)。
  • stable — 1 回の execute の間キャッシュされます(ターン中に安定した読み取り。例:time)。
  • volatile — ステートメントごとに再解決され、キャッシュされません(効果を伴う/非決定的な読み取り、およびすべての書き込み)。
クエリの実行方法(事前解決)

glove-sql は同期処理ですが、リソースは非同期で効果を伴うため、Database.execute はエンジン内部で解決をフックできません。そのため事前解決を行います。解析 → セキュリティゲート(ステートメント種別の許可リスト、デフォルトでは読み取り専用、CREATE/DROP は拒否、複数ステートメントは BEGIN … COMMIT スクリプトの場合のみ許可)→ 参照されるすべてのリレーションを収集 → WHERE/JOIN-ON の等価条件を引数としてプッシュダウン(必須キーがない場合は明確なエラー)→ 各リソースを 1 回ずつ解決 → 行をエンジンにマテリアライズ → 同期クエリを実行 → 一時テーブルを削除。最初に 1 回だけ解決することで、揮発性の保証が成り立ちます。

読み書きの一貫性:policy.readYourWrites(デフォルトは true)は、このセッションで実行された書き込みを、同じテーブルに対する後続の読み取りに反映します。そのため、上流のライブビューが追いついていなくても、INSERT した行は後続の SELECT に表示されます。

検出、トランザクション、EXPLAIN
  • 検出には information_schema を使います。 別途手順は不要です。リソースはカタログコールバックを介して information_schema.tables / .columns に公開されるため、エージェントは未知のデータベースに接続すると、テーブルを一覧表示し、列を調べて、自身に何ができるかを把握します。
  • トランザクション=プレビューとステージングです。 リソースへの書き込みは、副作用を伴うツール呼び出しです。BEGIN … COMMIT の中では実行されず、ステージングされ、正確なリゾルバーと引数とともに記録されます。db.preview()(および結果の staged フィールド)が承認のための画面となり、COMMIT ですべてを順番に実行し、ROLLBACK で破棄します。真のドライランです。トランザクション外での書き込みは即座に実行されます。Database の作成時に policy: { writes: true } を指定しない限り、書き込みは無効です。
  • EXPLAIN(db.explain(sql)、explain_sql ツール、または execute_sql を介した EXPLAIN <stmt>)は事前チェックのみを実行し、リゾルバー呼び出しは行いません。ステートメントが使用するリソース、それぞれの揮発性、読み取り/書き込みアクセス、および解決済みの引数を報告します。generate_image クエリの説明には費用がかかりません。
MCP サーバー → テーブル(glove-scratchpad/mcp)

ほとんどの MCP ツールは何らかのリソース型に対する CRUD なので、サーバーをリソースに分解し、それぞれにテーブルを割り当てます。glove-mcp は任意のピア依存関係です。ツールを公開対象から除外するには、connectMcp で excludeTools / filterTools を設定します。mcpResources / mountMcpDatabase は conn.listTools() をそのままブリッジするため、除外されたツールがテーブルになることはありません(mountMcp と同じ設定です)。より細かくブリッジごとに除外するには、table(tool) → null フックを使います。

import { connectMcp } from "glove-mcp";
import { mountMcpDatabase } from "glove-scratchpad/mcp";

const conn = await connectMcp({ namespace: "github", url, excludeTools: ["delete_repository"] });
await mountMcpDatabase(db, conn, {
  table: (t) => t.name === "list_pull_requests"
    ? { name: "github_pr", op: "select", volatility: "stable",
        schema: z.object({ title: z.string(), merged: z.boolean() }),
        rows: (d) => JSON.parse(d as string) }
    : null,                       // skip the rest, or map them too
});
// → INSERT INTO linear_issue SELECT … FROM github_pr WHERE merged  composes two servers in one statement.

読み取りツール(readOnlyHint)はデフォルトで select リソースになり、それ以外はデフォルトで揮発性の insert になります。MCP の結果には整った列リストが含まれないことが多いため、table(tool) を介して schema / columns(および rows 抽出関数)を宣言すると、デフォルトの単一 result 列を超えて、サーバーのデータを実際にクエリできるようになります。

バックエンド

操作用インターフェースは定義済みの Postgres サブセットで、バックエンドは差し替え可能です(ScratchpadBackend)。

  • glove-sql(デフォルト) — 依存関係なしの純粋な JavaScript 製 Postgres サブセットエンジンです。結合、GROUP BY / HAVING、CTE、集合演算、相関サブクエリ、ウィンドウ関数、jsonb アクセス、スカラー関数、information_schema、INSERT … SELECT、UPDATE に対応しています。サブセットの範囲外の処理では、誤った回答を返すのではなく、明確なエラーが発生します。
  • PgliteBackend(glove-scratchpad/pglite) — 完全な方言に対応する組み込み Postgres(WASM)です。@electric-sql/pglite は任意のピア依存関係です。
  • 独自バックエンドの用意 — 実際の Postgres、SQLite、またはリモートサービス上に ScratchpadBackend を実装します。
クイックリファレンス — 各要素の場所
用途シンボル
インタープリターの作成glove-scratchpad の Database.create({ policy?, backend?, actor? })
リソースの作成(Zod 優先)defineResource({ name, schema, keys?, volatility, select?, insert?, update?, delete? })
リソースの作成(列を明示)defineResource({ name, columns, volatility, … })
1 つのツール → 1 つのリソースresourceFromTool(tool, { name, volatility, schema | columns, op? })
Zod オブジェクト → 列(単独)columnsFromZod(schema, keys?)
リゾルバー内の型付きプッシュダウン引数TypedBindings<Row>(b.one("col") で自動補完)
SQL の実行/プレビューdb.execute(sql, opts) / db.explain(sql) / db.preview()
エージェントツール 1 つにまとめて初期化mountDatabase(glove, { db, prime?, explain?, allowWrites? })
MCP サーバー → テーブルglove-scratchpad/mcp の mcpResources / mountMcpDatabase
完全な Postgres 方言のバックエンドglove-scratchpad/pglite の PgliteBackend
BYO バックエンドのコントラクトScratchpadBackend

実行可能な API キー不要のガイドについては、examples/scratchpad-agent を参照してください(pnpm scratchpad:db)。

Display Stack のパターン

pushAndForget — 結果を表示(非ブロッキング)
async do(input, display) {
  const data = await fetchData(input);
  await display.pushAndForget({ input: data }); // Shows UI, tool continues
  return { status: "success", data: "Displayed results", renderData: data };
},
render({ data }) {
  return <Card>{data.title}</Card>;
},
renderResult({ data }) {
  return <Card>{(data as any).title}</Card>;  // Same card from history
},
pushAndWait — ユーザー入力を収集(ブロッキング)
async do(input, display) {
  const confirmed = await display.pushAndWait({ input }); // Pauses until user responds
  return {
    status: "success",
    data: confirmed ? "Confirmed" : "Cancelled",
    renderData: { confirmed },
  };
},
render({ data, resolve }) {
  return (
    <div>
      <p>{data.message}</p>
      <button onClick={() => resolve(true)}>Yes</button>
      <button onClick={() => resolve(false)}>No</button>
    </div>
  );
},
renderResult({ data }) {
  const { confirmed } = data as { confirmed: boolean };
  return <div>{confirmed ? "Confirmed" : "Cancelled"}</div>;
},
表示戦略
戦略動作用途
"stay"(デフォルト)スロットを常に表示します情報カード、結果
"hide-on-complete"スロットが解決されると非表示になりますフォーム、確認、選択
"hide-on-new"同じツールから新しいスロットが表示されると非表示になりますカートの概要、ステータスパネル
SlotRenderProps
プロパティ型説明
dataTpushAndWait/pushAndForget に渡される入力
resolve(value: unknown) => voidスロットを解決します。pushAndWait の場合、値が do に返されます。pushAndForget の場合、resolve() または removeSlot(id) を使用して閉じます。
reject(reason?: string) => voidスロットを拒否します。pushAndWait の場合、プロミスが拒否されます。キャンセル処理に使用します。

ツールの定義

defineTool(UI を備えたツールに推奨)
import { defineTool } from "glove-react";

const tool = defineTool({
  name: string,
  description: string,
  inputSchema: z.ZodType,              // Zod schema for tool input
  displayPropsSchema?: z.ZodType,      // Zod schema for display props (recommended for tools with UI)
  resolveSchema?: z.ZodType,           // Zod schema for resolve value (omit for pushAndForget-only)
  displayStrategy?: SlotDisplayStrategy,
  requiresPermission?: boolean | ((input: z.infer<I>) => boolean),  // function form gates per-input (e.g. only writes, not reads)
  unAbortable?: boolean,                 // Tool runs to completion even if abort signal fires (e.g. voice barge-in)
  do(input, display): Promise<ToolResultData>,  // display is TypedDisplay<D, R>
  render?({ props, resolve, reject }): ReactNode,
  renderResult?({ data, output, status }): ReactNode,
});

要点:

  • do() は { status, data, renderData } を返す必要があります。data はモデルに渡され、renderData はクライアント内にとどまります
  • render() は型付きの props(displayPropsSchema に対応)と型付きの resolve(resolveSchema に対応)を受け取ります
  • renderResult() は renderData を受け取り、履歴から読み取り専用のビューを表示します
  • displayPropsSchema は省略可能ですが、推奨されます。表示機能のないツールでは、代わりに生の ToolConfig を使用してください
ToolConfig(UI がないツール、または手動制御向け)
interface ToolConfig<I = any> {
  name: string;
  description: string;
  inputSchema?: z.ZodType<I>;          // Optional now — tools may use jsonSchema instead
  jsonSchema?: Record<string, unknown>; // Raw JSON Schema alternative (used by MCP-bridged tools)
  do: (input: I, display: ToolDisplay) => Promise<ToolResultData>;
  render?: (props: SlotRenderProps) => ReactNode;
  renderResult?: (props: ToolResultRenderProps) => ReactNode;
  displayStrategy?: SlotDisplayStrategy;
  requiresPermission?: boolean | ((input: I) => boolean);  // function form gates per-input
  unAbortable?: boolean;
}

jsonSchema と inputSchema の違い: いずれか一方だけを渡してください。inputSchema(Zod)は、do() の実行前にローカルで検証します。jsonSchema(生の JSON Schema)はそのままモデルに転送され、実行側では Zod による検証が省略されます。この場合、信頼できる情報源は別の場所にあります。MCP サーバーがスキーマを定義する bridgeMcpTool で使用されますが、外部ツールカタログをラップする際に直接使用することもできます。

build() 後の glove.fold

fold() は、build() 後を含め、いつでも IGloveRunnable 上で使用できます。検出サブエージェントの activate ツールはこの機能を利用し、新たにブリッジされた MCP ツールを会話中に取り込んで、次のターンから使えるようにします。「ツールを動的に登録する」パターン全般に役立ちます。

const agent = new Glove({...}).build();
// ...later, mid-conversation:
agent.fold({ name: "new_tool", description: "...", inputSchema: z.object({}), async do() { ... } });
do(input, display, glove, signal?) — 第 3、第 4 引数

ツールの do 関数は、第 3 引数として実行中の IGloveRunnable を、第 4 引数(省略可能)としてアクティブなリクエストの AbortSignal を受け取ります。glove 引数を使うと、discovermcp サブエージェントの activate ツールはメインエージェントに戻り、ブリッジされた MCP ツールを折り込んで、メインエージェントのモデルと displayManager を引き継ぐことができます。signal 引数は、長時間実行される内部処理(ネストされたエージェントの実行、フェッチなど)に渡して、中断が伝播するようにしてください。これを無視するツールでも、実行側の中断可能なプロミスの巻き戻しが自動で適用されます。また、unAbortable: true が指定されたツールは signal を完全に無視する必要があります。ほとんどのツールは両方を無視します。

ToolResultData
interface ToolResultData {
  status: "success" | "error";
  data: unknown;                 // Sent to the AI model
  message?: string;              // Error message (for status: "error")
  renderData?: unknown;          // Client-only — NOT sent to model, used by renderResult
  summary?: string;              // Populated by the Executor from tool.generateSummary; swapped in for data in older context when enableToolResultSummary is on
  generateSummaryArgs?: unknown; // Opaque payload do() returns to drive the tool's generateSummary handler
}

重要: モデルアダプターは、AI に送信する前に renderData を明示的に削除します。そのため、機密性の高いクライアント専用データ(メールアドレスや UI の状態など)を renderData に安全に保存できます。

ツール実行結果の要約(オプトイン)

ペイロードによってコンテキストが肥大化するツール(ファイル読み取り、ウェブ取得、大量のクエリ結果)のトークン効率を最適化します。デフォルトでは無効です。GloveConfigでenableToolResultSummary: trueを指定し、圧縮したい各ツールにgenerateToolSummaryを追加してオプトインします。

const agent = new Glove({
  store,
  model: createAdapter({ provider: "anthropic" }),
  displayManager: new Displaymanager(),
  systemPrompt: "...",
  compaction_config: { compaction_instructions: "Summarize so far." },
  enableToolResultSummary: true,            // turn on the pruner
})
  .fold({
    name: "read_file",
    description: "Read a slice of a file.",
    inputSchema: z.object({ path: z.string(), from: z.number().optional(), to: z.number().optional() }),
    async do(input) {
      const slice = await readSlice(input);
      return {
        status: "success",
        data: slice,
        generateSummaryArgs: { path: input.path, from: input.from, to: input.to, lineCount: slice.split("\n").length },
      };
    },
    async generateToolSummary(args) {
      const { path, from, to, lineCount } = args as any;
      const range = from != null || to != null ? ` lines ${from ?? 1}-${to ?? "EOF"}` : "";
      return `Read ${path}${range} (${lineCount} lines).`;
    },
  })
  .build();

仕組み:

  1. do()がgenerateSummaryArgsを返します — 要約ハンドラーに必要な情報(パスと行範囲、URL、クエリ、行数など)を返します。
  2. Executorがdo()の解決後にgenerateToolSummary(args)を呼び出し、結果をToolResultData.summaryに保存します。結果にはdataとsummaryの両方が含まれます。
  3. **PromptMachine.summarizeOlderToolResults**は、モデルを呼び出すたびに(enableToolResultSummary: trueの場合)実行され、最新のツール以外のユーザーメッセージを見つけ、そのインデックス以前のすべてのツール実行結果について、dataをsummaryに置き換えます。現在のターンのツール実行結果は変更されません。

ストアには常にdataとsummaryの両方が保持されます。書き換えられるのはモデルアダプターに渡すメッセージだけです。トランスクリプトのレンダラー、履歴スナップショット、分析機能には、引き続き完全な記録が表示されます。

generateToolSummaryがないツールや、generateSummaryArgsを省略した呼び出しでは、summaryは未設定のままとなり、プルーナーはそのままにします。一部のツールだけが計測されているツールカタログでも問題なく動作します。

コンパクションと併用できます。ツールの要約により、Observerがコンパクションを実行するタイミングを遅らせることができます。また、計測対象のコンテキストがCONTEXT_COMPACTION_LIMITを超えると、引き続きコンパクションが実行されます。

<Render> コンポーネント

手動でのタイムライン描画を置き換える、ヘッドレスのレンダーコンポーネントです。

import { Render } from "glove-react";

<Render
  glove={gloveHandle}           // return value of useGlove()
  strategy="interleaved"        // "interleaved" | "slots-before" | "slots-after" | "slots-only"
  renderMessage={({ entry, index, isLast }) => ...}
  renderToolStatus={({ entry, index, hasSlot }) => ...}
  renderStreaming={({ text }) => ...}
  renderInput={({ send, busy, abort }) => ...}
  renderSlotContainer={({ slots, renderSlot }) => ...}
  as="div"                      // wrapper element
  className="chat"
/>

機能:

  • displayStrategyに基づくスロットの自動表示制御
  • 完了したツールのrenderDataに対するrenderResultの自動描画
  • インターリーブ:スロットを関連するツール呼び出しの横にインラインで表示
  • すべてのレンダープロパティに適切なデフォルト値を設定

GloveHandle インターフェース

useGlove()が返す、<Render>が使用するインターフェースです。

interface GloveHandle {
  timeline: TimelineEntry[];
  streamingText: string;
  busy: boolean;
  sessionReady: boolean;
  sessionId: string;
  slots: EnhancedSlot[];
  sendMessage: (text: string, images?: { data: string; media_type: string }[]) => void;
  abort: () => void;
  renderSlot: (slot: EnhancedSlot) => ReactNode;
  renderToolResult: (entry: ToolEntry) => ReactNode;
  resolveSlot: (slotId: string, value: unknown) => void;
  rejectSlot: (slotId: string, reason?: string) => void;
}

useGlove フックの戻り値

プロパティ型説明
timelineTimelineEntry[]メッセージとツール呼び出し
streamingTextstring現在のストリーミングバッファー
busybooleanエージェントが処理中かどうか
sessionReadyboolean非同期のgetSessionIdが解決するまでfalseです。設定されていない場合は常にtrueです
sessionIdstring解決されたセッションIDです。何も設定されていない場合は、自動生成されたglove_<uuid>になります(ゼロ設定のセッション)
newConversation(id?)Promise<string>現在の場所で新しい会話を開始します。IDを生成し(引数 → GloveClient.createSessionId → 自動生成)、処理中の作業を中断してタイムラインをリセットし、ストアとエージェントを再構築します。再マウントは不要です。
switchConversation(id)void既存のセッションに現在の場所で切り替えます。ストアを切り替え、タイムラインを再読み込みします。sessionId設定プロパティもリアクティブで、変更すると同じ処理が行われます。
isCompactingbooleanコンテキストのコンパクションが進行中かどうか(compaction_start/compaction_endイベントによって更新)
slotsEnhancedSlot[]メタデータを含むアクティブな表示スタック
tasksTask[]エージェントのタスクリスト
inboxInboxItem[]受信トレイのアイテム(保留中、解決済み、消費済み)
statsGloveStats{ turns, tokens_in, tokens_out }
sendMessage(text, images?)voidユーザーメッセージを送信
abort()void現在のリクエストをキャンセル
renderSlot(slot)ReactNode表示スロットを描画
renderToolResult(entry)ReactNode履歴からツール実行結果を描画
resolveSlot(id, value)voidpushAndWaitスロットを解決
rejectSlot(id, reason?)voidpushAndWaitスロットを拒否

TimelineEntry

type TimelineEntry =
  | { kind: "user"; text: string; images?: string[] }
  | { kind: "agent_text"; text: string }
  | { kind: "tool"; id: string; name: string; input: unknown; status: "running" | "success" | "error"; output?: string; renderData?: unknown };

type ToolEntry = Extract<TimelineEntry, { kind: "tool" }>;

サポート対象プロバイダー

プロバイダー環境変数デフォルトモデルSDK 形式
openaiOPENAI_API_KEYgpt-4.1openai
anthropicANTHROPIC_API_KEYclaude-sonnet-4-20250514anthropic
openrouterOPENROUTER_API_KEYanthropic/claude-sonnet-4openai
geminiGEMINI_API_KEYgemini-2.5-flashopenai
minimaxMINIMAX_API_KEYMiniMax-M2.5openai
kimiMOONSHOT_API_KEYkimi-k2.5openai
glmZHIPUAI_API_KEYglm-4-plusopenai
mimoMIMO_API_KEY (+ optional MIMO_BASE_URL)mimo-v2.5mimo
ollama(none)(user-specified)openai
lmstudio(none)(user-specified)openai
bedrockAWS_ACCESS_KEY_IDanthropic.claude-3-5-sonnet-20241022-v2:0bedrock
推論モデル

OpenAI 互換アダプターは、DeepSeek-R1 / V4、Qwen3-Thinking、GLM-4.5 / 4.6、Kimi K2、MiniMax M2.5、OpenRouter、GPT-5 / o-series、およびこの規約に従うその他の OpenAI 形式のエンドポイントから、プロバイダーが出力する推論トレース(reasoning_content / reasoning フィールド)を取得します。取得したトレースは Message.reasoning_content(型付き文字列フィールド)に格納され、その後のツール呼び出しを行うアシスタントのターンで返されます(DeepSeek V4 と MiMo は、そうしないとリクエストを拒否します)。

用途設定
デフォルトの取得と返却createAdapter({ provider, reasoning: true })
思考の深さを指定(GPT-5 / GLM / MiniMax / Kimi / DeepSeek V4)createAdapter({ provider, reasoning: { effort: "high" } }) — "minimal"/"low"/"medium"/"high"
OpenRouter の統一推論オブジェクトcreateAdapter({ provider: "openrouter", reasoning: { reasoningObject: { effort: "high", max_tokens: 2000 } } })
Anthropic 形式の thinking(OpenAI シム向け)createAdapter({ provider, reasoning: { thinking: { type: "enabled", budget_tokens: 4000 } } })
Qwen3 dashscope の enable_thinkingcreateAdapter({ provider, reasoning: { extraBody: { enable_thinking: true, thinking_budget: 1024 } } })
トレースを表示テキストに含める(`` で囲む)createAdapter({ provider, reasoning: { includeInText: true } })
返却を無効化(DeepSeek-R1 専用)createAdapter({ provider, reasoning: { echo: false } })

OpenAICompatReasoningOptions は glove-core/models/openai-compat からエクスポートされています。createAdapter と createChatHandler の従来の reasoningEffort / includeReasoningInText フィールドは新しい形式に統合されるため、既存の MiMo 呼び出し元は変更なしで動作します。MiMo プロバイダーは専用アダプターを引き続き使用します(MimoAdapter にはこのフィールドがすでに組み込まれています)。

ビルド済みツールレジストリ

https://glove.dterminal.net/tools から利用できます。コピーしてプロジェクトに貼り付けてください。

  • confirm_action — はい/いいえの確認ダイアログ
  • collect_form — 複数フィールドのフォーム
  • ask_preference — 単一選択の優先項目ピッカー
  • text_input — 自由記述入力
  • show_info_card — 情報/成功/警告カード(pushAndForget)
  • suggest_options — 複数選択式の候補
  • approve_plan — 手順ごとのプラン承認

音声連携(glove-voice)

パッケージの概要
パッケージ用途インストール
glove-voice音声パイプライン:GloveVoice、アダプター(STT/TTS/VAD)、AudioCapture、AudioPlayer、SpeechGate、AudioIO プラットフォーム境界pnpm add glove-voice
glove-voice-nativeReact Native / Expo の音声バックエンド:NativeAudioCapture、NativeAudioPlayer、createNativeAudioIO、SileroVADNativeAdapter(サブパス /silero-vad)。ネイティブモジュール:react-native-audio-api(必須のピア依存関係)、onnxruntime-react-native + expo-file-system(ニューラル VAD 用のオプション)。開発クライアント / prebuild のみ対応 — Expo Go では動作しません。pnpm add glove-voice-native
glove-react/voiceReact フック:useGloveVoice、useGlovePTT、VoicePTTButton(DOM に依存しないため、React Native でも使用可能)glove-react に含まれます
glove-nextトークンハンドラー:createVoiceTokenHandler(すでに glove-next に含まれており、個別のインポートは不要です)glove-next に含まれます
glove-voice-s2s音声間変換:RealtimeAgent、s2sDrivenModel、createS2SAdapter、型付きターンテイキング設定(詳しくは下記「音声間変換、アバター、LiveKit」をご覧ください)ワークスペース
glove-voice-avatarS2S スタック上のライブアバター:AvatarAdapter、TavusEchoAdapter、AnamPassthroughAdapter、attachAvatarワークスペース
glove-voice-livekitLiveKit トランスポート + LiveKit アバター:LiveKitTransport、attachRealtime、TavusLiveKitAvatar、AnamLiveKitAvatar、トークンの発行ワークスペース
アーキテクチャ
Mic → VAD → STTAdapter → glove.processRequest() → TTSAdapter → Speaker

GloveVoice は、Glove インスタンスを全二重の音声パイプラインでラップします。インテリジェンス層は引き続き Glove が担い、すべてのツール、表示スタック、コンテキスト管理は通常どおり動作します。STT と TTS は交換可能なアダプターです。テキストトークンは SentenceBuffer を通って TTS にリアルタイムで送られます。

クイックスタート(Next.js + ElevenLabs)

ステップ 1:トークンルート — API キーを短期間有効なトークンに交換するサーバーサイドハンドラーです。

// app/api/voice/stt-token/route.ts
import { createVoiceTokenHandler } from "glove-next";
export const GET = createVoiceTokenHandler({ provider: "elevenlabs", type: "stt" });
// app/api/voice/tts-token/route.ts
import { createVoiceTokenHandler } from "glove-next";
export const GET = createVoiceTokenHandler({ provider: "elevenlabs", type: "tts" });

.env.local に ELEVENLABS_API_KEY を設定します。

ステップ 2:クライアントの音声設定

// app/lib/voice.ts
import { createElevenLabsAdapters } from "glove-voice";

async function fetchToken(path: string): Promise<string> {
  const res = await fetch(path);
  const data = await res.json();
  return data.token;
}

export const { stt, createTTS } = createElevenLabsAdapters({
  getSTTToken: () => fetchToken("/api/voice/stt-token"),
  getTTSToken: () => fetchToken("/api/voice/tts-token"),
  voiceId: "JBFqnCBsd6RMkjVDRZzb",
});

ステップ 3:SileroVAD — SSR に安全な動的インポート

export async function createSileroVAD() {
  const { SileroVADAdapter } = await import("glove-voice/silero-vad");
  const vad = new SileroVADAdapter({
    positiveSpeechThreshold: 0.5,
    negativeSpeechThreshold: 0.35,
    wasm: { type: "cdn" },
  });
  await vad.init();
  return vad;
}

ステップ 4:React フック

const { runnable } = useGlove({ tools, sessionId });
const voice = useGloveVoice({ runnable, voice: { stt, createTTS, vad } });
// voice.mode, voice.isActive, voice.isMuted, voice.error, voice.transcript
// voice.start(), voice.stop(), voice.interrupt(), voice.commitTurn()
// voice.mute(), voice.unmute()              — gate mic audio to STT/VAD
// voice.narrate("text")                     — speak text via TTS without model (returns Promise)
startMuted 設定オプション

手動(プッシュトゥトーク)モードでは、パイプラインはデフォルトでミュート状態から起動するため、start() の直後に mute() を呼び出す必要はありません。これにより、その間に音声が漏れる競合状態を防げます。

// Manual mode auto-mutes — just works
await voice.start(); // already muted in manual mode

// Explicit override
const voice = useGloveVoice({
  runnable,
  voice: { stt, createTTS, turnMode: "manual", startMuted: false }, // opt out
});
useGloveVoice の enabled 状態

このフックは voice.enabled を公開するようになりました。これはユーザーの意図を追跡し(start() 後は true、stop() の後またはパイプライン停止時は false)、手動での useState と同期用 useEffect のパターンに代わるものです。

// Before — consumer tracks + syncs
const [voiceEnabled, setVoiceEnabled] = useState(false);
useEffect(() => {
  if (voiceEnabled && !voice.isActive) setVoiceEnabled(false);
}, [voiceEnabled, voice.isActive]);

// After — hook tracks it
voice.enabled  // auto-resets on pipeline death
useGlovePTT フック(プッシュトゥトーク)

PTT のライフサイクル全体をカプセル化する高水準フックです。約 80 行の定型コードを約 5 行に減らせます。

import { useGlovePTT } from "glove-react/voice";

const glove = useGlove({ endpoint: "/api/chat", tools });
const ptt = useGlovePTT({
  runnable: glove.runnable,
  voice: { stt, createTTS },    // turnMode forced to "manual"
  hotkey: "Space",               // default, auto-guards INPUT/TEXTAREA
  holdThreshold: 300,            // click-vs-hold discrimination (ms)
  minRecordingMs: 350,           // min audio before committing
});

// ptt.enabled      — is the pipeline active
// ptt.recording    — is the user currently holding
// ptt.processing   — is STT finalizing
// ptt.mode         — voice mode (idle/listening/thinking/speaking)
// ptt.transcript   — partial transcript while recording
// ptt.error        — last error
// ptt.toggle()     — enable/disable the pipeline
// ptt.interrupt()  — barge-in
// ptt.bind         — { onPointerDown, onPointerUp, onPointerLeave }

<button {...ptt.bind}><MicIcon /></button>
<VoicePTTButton> コンポーネント

マイクボタン用のレンダープロパティを備えた、ヘッドレス(スタイルなし)コンポーネントです。

import { VoicePTTButton } from "glove-react/voice";

<VoicePTTButton ptt={ptt}>
  {({ enabled, recording, mode }) => (
    <button className={recording ? "active" : ""}>
      <MicIcon />
      {enabled && <StatusDot />}
    </button>
  )}
</VoicePTTButton>

クリックと長押しの判別、ポインターが離れた場合の安全対策、aria 属性を備えています。

<Render> の音声サポート

<Render> にオプションの voice プロパティを渡すと、トランスクリプトと音声ステータスが自動的にレンダリングされます。

<Render
  glove={glove}
  voice={ptt}                    // or useGloveVoice() return
  renderTranscript={...}         // optional custom renderer
  renderVoiceStatus={...}        // optional custom renderer
  renderInput={() => null}
/>
ターンモード
モード動作用途
"vad"(デフォルト)音声を自動検出 + 割り込みハンズフリーで音声を優先するアプリ
"manual"プッシュトゥトーク、明示的な commitTurn()騒がしい環境、正確な制御が必要な場合
ノイズ耐性(音声ゲーティング)

"vad" モードでは、マイク音声はデフォルトで音声ゲーティングされます(speechGating: true)。音声はローリング式のプリロールバッファ(speechGatePrerollMs、デフォルトは800ms)に保持され、VADが音声区間を確認した場合にのみSTTプロバイダーへ送られます。背景ノイズがSTTに届くことはないため、文字起こしされたり、誤って言葉として認識されたりすることはありません。SileroVADAdapter(supportsRealStart: true を宣言)を使用する場合:

  • ゲートは speech_real_start(音声が minSpeechMs、デフォルトは250msを超えた場合)で開きます。短いノイズのバーストは vad_misfire として報告され、その音声は完全に破棄されます。
  • バージインも speech_real_start で発生するため、ドアが勢いよく閉まってもエージェントの発話は中断されません。
  • すべてのVADアダプターはフレームごとに speech_prob を出力します(モデルの確率、または組み込みの VAD の場合はエネルギーを基にした値)。レベルメーターやしきい値の調整に使用してください。

組み込みのエネルギー方式の VAD は時間ベース(チャンクサイズに依存しない silenceMs、minSpeechMs)で、デフォルトで有効な適応型ノイズフロア(adaptive: true)により、安定した背景ノイズを上回るよう実効しきい値が引き上げられます。AudioCapture は標準的な制約と併せて voiceIsolation を要求します。GloveVoiceConfig.micConstraints で任意の制約を上書きできます。連続したSTTストリーミングに戻すには、speechGating: false を設定してください。

React Native / Expo(glove-voice-native)

パイプラインはプラットフォームに依存せず、プラットフォーム固有なのはマイクのキャプチャとスピーカーの再生だけです。これらは GloveVoiceConfig.audio(AudioIO。デフォルトはブラウザー実装)から注入します。React Native / Expoでは:

import { createNativeAudioIO, withNativeAudio } from "glove-voice-native";
import { SileroVADNativeAdapter } from "glove-voice-native/silero-vad";

const vad = new SileroVADNativeAdapter();   // Silero v5 on onnxruntime-react-native
await vad.init();                            // downloads + caches model (expo-file-system)

const voice = useGloveVoice({
  runnable,
  voice: withNativeAudio({ stt, createTTS, vad }),  // or { ..., audio: createNativeAudioIO() }
});
  • react-native-audio-api(Software Mansion)を基盤とし、キャプチャには AudioRecorder、再生にはWeb Audio実装を使用します。Expo設定プラグインがマイクの権限を処理します(iosMicrophonePermission, androidPermissions: ["android.permission.RECORD_AUDIO", ...])。
  • NativeAudioCapture はiOSセッションを playAndRecord と voiceChat モードに設定し、OSのエコーキャンセリングを有効にします(ブラウザーの echoCancellation 制約に相当します)。
  • ElevenLabsのSTT/TTSアダプターはRNでも変更せずに動作します(WebSocketと純粋なJavaScriptによるbase64。btoa/atob は不要です)。
  • ネイティブモジュールを使用するには、Expo開発クライアントまたは expo prebuild が必要です。Expo Goは使用できません。
  • onnxruntime-react-native を使用しない場合は、glove-voice のエネルギー方式のVADが、ネイティブ依存関係のない代替手段となります。
ナレーションとマイク制御
  • voice.narrate(text) — モデルを介さず、任意のテキストをTTSで読み上げます。音声の再生が終わると処理が完了します。ナレーション中はマイクが自動的にミュートされます。interrupt() で中断できます。pushAndWait のツールハンドラーから安全に呼び出せます。
  • voice.mute() / voice.unmute() — STT/VADへのマイク音声の転送を制御します。ミュート中も audio_chunk イベントは発生します(可視化用)。
  • audio_chunk イベント — ミュート中も出力される、マイクからの未加工の Int16Array PCMです。波形やレベルの可視化に使用できます。
  • コンパクション中の無音化 — 音声はコンテキストのコンパクション中に text_delta を自動的に無視するため、要約が読み上げられることはありません。
音声優先のツール設計
  • pushAndWait の代わりに pushAndForget を使用する — クリックを待つブロッキングツールは、音声モードでは使用できません。
  • data に説明的なテキストを返す — LLMがこれを読み取り、応答を音声で伝えられるように組み立てます。
  • 音声専用のシステムプロンプトを追加する — エージェントに、結果を簡潔に読み上げるよう指示します。
  • スロットの読み上げには narrate() を使用する — ツールハンドラーから表示内容を音声で読み上げます。
対応音声プロバイダー
プロバイダートークンハンドラーの設定環境変数
ElevenLabs{ provider: "elevenlabs", type: "stt" | "tts" }ELEVENLABS_API_KEY
Deepgram{ provider: "deepgram" }DEEPGRAM_API_KEY
Cartesia{ provider: "cartesia" }CARTESIA_API_KEY

音声間変換、アバター、LiveKit

上記のカスケードパイプライン(glove-voice)は、2 つある音声経路のうちの 1 つです。もう 1 つは、エージェントをリアルタイムの音声間変換モデル上で直接実行します。STT/TTS はなく、モデル自体が音声を担います。

import { RealtimeAgent, s2sDrivenModel } from "glove-voice-s2s";

const agent = new Glove({
  store,
  // The model slot CARRIES the realtime config — the agent definition is the
  // single source of truth; RealtimeAgent derives the provider session from it.
  model: s2sDrivenModel({
    label: "s2s-front",
    provider: "openai",              // or "gemini"
    apiKey: process.env.OPENAI_API_KEY!,
    // model/voice optional — defaults: gpt-realtime + marin / gemini-live-2.5-flash-preview + Puck
    turnDetection: { type: "server_vad", silence_duration_ms: 450, prefix_padding_ms: 300 }, // snappy barge-in
  }),
  displayManager: new Displaymanager(),
  systemPrompt: "...",
  serverMode: true,
}).fold({ /* tools work normally */ });

const rt = new RealtimeAgent({ agent });   // derives the adapter from the model slot
await rt.start();
rt.sendAudio(pcm);                          // caller mic in (Int16Array at rt.adapter.inputFormat.sampleRate)
rt.adapter.on("audio", (pcm, fmt) => {});   // agent speech out (24 kHz)
rt.adapter.on("interrupted", () => {});     // barge-in — flush your playback
rt.inject("<text>", { respond: true });     // typed lines / worker results into the live session

実証済みのレイヤー構成(examples/s2s-rooms 以降)では、S2S モデルは薄いフロントエージェントです。負荷の高い検索は、glove-mesh 経由で高性能なワーカーに委任し、各呼び出しはステーションシグナルの実行として扱います。

アバターは、このスタック上のレンダリングレイヤーです。PCM を受け取り、WebRTC サーフェス上に話す顔を表示します。AvatarAdapter の実装は次のとおりです。TavusEchoAdapter(Tavus のエコー PAL と Daily ルーム。Tavus のインタラクションは Daily データチャネル専用のため、sendInteraction のクーリエが必要です)、AnamPassthroughAdapter(Anam のパススルーペルソナ。ブラウザー SDK が音声入力を管理するため、sendCommand のクーリエが必要です。Anam のプラン上限により、Growth ティア未満では数分ごとにセッションが強制終了されます。ホストは disconnect() と connect() でセッションを更新し、クライアントを再アタッチします)。接続は 1 回の呼び出しで完了します。const detach = await attachAvatar(rt, avatar)(音声 → sendAudio、発話停止 → endUtterance、中断 → interrupt)。

LiveKit(glove-voice-livekit)は、手作りの音声配管を双方向の WebRTC に置き換えます。音声経路には LiveKitTransport と attachRealtime(rt, transport) を使います(ブラウザー側は Room.connect と setMicrophoneEnabled だけになり、割り込み処理はサーバー側の clear() で行います)。顔には TavusLiveKitAvatar/AnamLiveKitAvatar を使います。プロバイダーのワーカーは参加者としてあなたのルームに参加し(mintAvatarToken でトークンを発行し、ID は TAVUS_AVATAR_IDENTITY/ANAM_AVATAR_IDENTITY、エージェントを onBehalfOf に指定します)、lk.audio_stream のバイトストリーム経由で音声を受け取ります。アバターをアタッチする場合は publishAgentAudio: false を設定してください(ワーカーがエージェントに代わって音声を公開するため、二重に公開しないでください)。

完全な API は api-reference.md(「glove-voice-s2s」、「glove-voice-avatar」、「glove-voice-livekit」)にあります。実行可能な手順は examples/layered-voice → server-voice → s2s-rooms → avatar-rooms → livekit-rooms です(各例を保持し、ポートは並行して利用できます)。

作業環境(glove-working-environment)

固定されたツールメニューではなく、モデルがコードを書くために使うファイルシステムとスクリプトランタイムです。ドキュメント、スプレッドシート、デッキ、アーカイブなど、ファイルを扱う作業に決まった終わりがなく、次の有用な手順が呼び出しではなくスクリプトとなる場合に使用します。

import { createWorkingEnvironment, mountWorkingEnvironment } from "glove-working-environment";
import { documents } from "glove-env-documents";
import { spreadsheets } from "glove-env-spreadsheets";

const env = await createWorkingEnvironment({
  stdlib: [documents(), spreadsheets()],
  limits: { runTimeoutMs: 60_000 },
  execution: { size: 1 },
});

mountWorkingEnvironment(agent, { env });   // structural — no glove-core dependency
ツリー

/inbox(ホストからマウントされた入力)・/scripts(モデルの永続ライブラリ。生成された .d.ts の兄弟ファイルを含みます)・/skills(作成済みのレシピ。起動時に実体化されます)・/std(モジュールごとに1つのディレクトリがあり、型とREADMEを格納します)・/tmp・/out(成果物)・/.env(管理情報)。

ホストとの連携口:入力には env.mount(bytes|path|{text}, dest)、出力には env.export(glob)、永続化には env.snapshot() / fromSnapshot、ワーカースレッドの解放には env.close() を使います。

作業の受け渡し。 onPresent({ path, name, bytes, mediaType, caption }) を設定すると present 動詞が追加されます。設定しない場合は追加されません。/out に書き込むとファイルが作成され、present はファイルを受け渡します。タスクの終了時点では、/out には下書きや中間生成物も含まれるため、どれが回答だったかを把握しているのはエージェントだけです。/out 以外のパスは修正方法とともに拒否されます。mediaType は拡張子から取得されます(拡張子のないファイルに限り、マジックバイトを使用します)。そのため、ホストはダウンロードのプロンプトではなく、プレーヤーを選べます。

読み取り専用ゾーン。 readOnlyPaths: ["/corpus"] を指定すると、サブツリーを読み取り、grepし、説明できますが、書き込みはできません。適用は単一のコア変更ゲートウェイで行われるため、1つのチェックですべてのインターフェースを制御できます。対象はモデルの動詞、env:fs を経由するスクリプト、アダプターのハンドル、undo です。env.mount() は意図的にこの制限を迂回します。エージェントが読み取りだけできるコンテンツを用意することが目的だからです。一方、ホスト用のガード付きハンドルである env.fs には、モデルと同じルールが適用されます。ゾーンはオリエンテーションファイルで告知され、起動時に mkdir されます。拒否されることでなく、読み取ることで境界を理解できるようにするためです。「実際のプロジェクトを用意し、/src は参照専用にする」という用途では、hostDirectory と組み合わせます。スナップショットには保存されません。stdlib と同様に、ホストが再度提供します。

ツリーの保存場所。 filesystem には任意の Vfs を指定できます。標準で3種類が用意されています。inMemoryFs()(デフォルト)、hostDirectory(dir)(実ディレクトリ上のコピーオンライト方式。エージェントは commit() するまで元のディレクトリを変更できません)、cachedRemote(store, { prefix })(オブジェクトストレージ。get / put / delete / list を指定するため、SDK への依存はありません)。再起動をまたぐ単純な永続化には、1つのオブジェクトへの snapshot() を推奨します。ファイルごとではなくセッションごとに1回のラウンドトリップで済み、アトミックだからです。cachedRemote は構造インデックス(パス、サイズ、mtime、ディレクトリ)をメモリに保持します。totalSize() は書き込みのたびに実行され、files() は glob / grep / rm -r を支えるためです。ネットワークを介するのはコンテンツだけです。分散ロックには対応していないため、セッションごとに1つのプレフィックスを使用してください。

機能を公開する4つの経路 — 形状に応じて選択します

実装者が選択を誤りがちな点です。対象の形状によって経路が決まります。

形状経路呼び出しスタイル
I/O を行う — ファイルの読み書き、外部への呼び出しdefineAdapterasync
状態を持つビルダー — new X()、メソッドチェーンによる変更、最後に保存defineBuilder / defineBuildersasync
純粋な計算 — I/O なし、状態なしdefinePureModule同期
ライブラリではなく機能 — MCP サーバー、モデル、HTTP APIdefineToolsasync

defineTools({ name, description, fns, docs }) は、ToolFn のリストを env: モジュールとしてマウントします。構造は glove-scratchpad/fns カタログと同じなので、fnsFromMcp(conn) がリストを直接生成し、アダプターを書かずに MCP サーバーをインポート可能にできます。これを使う理由はループの効率です。動詞を使うと、回答のたびにコンテキストウィンドウが埋まるため、レンダリング済みページを40件確認するには40回のラウンドトリップが必要です。同じ機能を関数として使えば、スクリプトでループして1行だけ返せます。モジュールの形状もインポート文も同じで、手書きの types も不要です。

// Pure computation: imported INSIDE the worker, bound into the vm context.
// Calls never cross a thread, so they stay synchronous.
definePureModule({
  name: "lodash",
  from: "lodash",
  description: "Lodash utilities for shaping data.",
  pick: ["groupBy", "sumBy", "orderBy", "uniqBy", "camelCase"],
})

pick はサンドボックスの境界であり、利便性のための機能ではありません — 選択した関数は vm の外にあるワーカーのレルムで実行されます。文字列をコードとして扱うメンバーは決して選択しないでください(_.template はホスト側で Function(source) を実行します)。プロトタイプのメンバーは定義時に拒否されます。環境の作成時に、すべての名前が実際のモジュールに対して検証されるため、スクリプト内で undefined になるのではなく、その時点でタイプミスが検出されます。

同期が重要な理由:通常の値に対する await は何もしませんが、promise に対する await の付け忘れは気づきにくいゴミを生みます — 非同期バインディング経由で sumBy(rows, 'n') を呼び出すと、文字列化された promise が返され、実行は成功として報告されます。同期にするほうが間違いが起きても安全なので、同期ライブラリは同期的に公開する必要があります。

defineBuilder はビルダー API を変更せずに公開します — 呼び出しはコンテキスト内でフラットな操作リストとして記録され、最後の呼び出しでホスト側にて再実行されるため、new Pptx()、メソッドチェーン、一方のノードをもう一方に渡す操作がすべてワーカー境界を越えて動作します。glove-env-slides と glove-env-spreadsheets はこれを使い、pptxgenjs と exceljs をそのまま公開しています。

HTTP ファイルとスコープ付きシークレット

glove-env-fetch の fetchFiles() を使って、JSON、フォーム、マルチパートのボディを含む env:fetch.request/download/upload を利用します。ホスト側で管理するキーのメタデータや参照には、glove-env-secret の secret() を使います。ホストのセットアップ、スクリプト API、安全性の上限、キャンセル、永続化については、HTTP files and host secrets を参照してください。シェルや curl のブリッジを独自に作ったり、認証情報を VFS ファイルに保存したりしないでください。

ネットワーク許可と認証情報のエイリアスは、ホスト設定に保持してください。ネイティブの fetch は、公開されていない DNS/IP の宛先をブロックします。プライベートサービスには、privateNetworkOrigins による個別のオプトインが必要です。代替トランスポートにも、同等の DNS/IP/TLS 適用を実装する必要があります。永続化する SecretStore はテナントごとにスコープし、復元時に再度渡してください。参照だけではアクセス権は付与されず、値の開示や書き込みはデフォルトで無効です。アダプターの呼び出しは、スクリプトのデフォルトエクスポート内に置いてください。実行終了時にホストの I/O を中断できるよう、バインディング内で ctx.signal を読み取ってください。

アダプターの作成とテスト

defineAdapter({ name, description, types, docs, skills?, create(vfs, ctx) })。create は機能の境界であり、書き込み時のスクリプト検証用の読み取り専用として一度、合計 2回 呼び出されます。公開されるすべての関数はラップされるため、失敗は env:<name>.<fn>: … の形式で表示され、引数はホストレルムの値としてディープコピーされて渡されます。

glove-working-environment/testing を使ってテストします。createAdapterTestEnv(adapter) は { env, fs, script(), runScript(), audit() } を返します。audit() は、types と実際のバインディングがどちらかの方向で食い違っている場合、ビルドを失敗させます — pure module では Promise のルールが逆転する場合も含みます(同期バインディングに Promise<…> を宣言するのが、その場合の誤りです)。

Next.js でのホスティング

原因から遠く離れた場所で表面化する落とし穴が 2 つあります。

  1. すべての環境パッケージを serverExternalPackages に指定する必要があります。 ワーカープールは自身のモジュール URL を基準にエントリーポイントを特定します。バンドルすると、その URL は Next のチャンク内を指し、ワーカーはその隣に存在しなくなります。ビルド時ではなく、最初の run_script で失敗します。sharp(env:images 経由)も、別の理由で外部化する必要があります。
  2. pnpm モノレポでは、それだけでは不十分です。 Next は解決済みのパスに対して serverExternalPackages の指定を照合し、symlinks: true をハードコードして解決します。ワークスペースリンクは ../../packages/<name> に解決されますが、このパスには node_modules のセグメントがないため、照合が行われません。症状は Can't resolve './worker-dev.mjs' です。ヒューリスティックを回避するため、webpack の外部依存関係を明示的に追加してください。

エージェントはサーバー側で実行する必要があります。createChatHandler はクライアント側ツール用のモデルプロキシなので、この場合には適していません。ルートで processRequest を実行し、エージェント自身のイベントストリームを SSE として転送してください。examples/document-desk を参照してください。

モーションのレンダリング(glove-env-motion)

motion() は limits: MOTION_LIMITS と一緒にマウントしてください。この 2 つはセットです。1 フレームはブラウザーのスクリーンショット(約 330ms)なので、環境の runTimeoutMs でレンダリングを完了できない場合、開始前に拒否されます。拒否時には設定すべき正確な limits: { runTimeoutMs: N } が示されますが、それでも拒否であることに変わりはありません。Next では、glove-env-motion と playwright-core を serverExternalPackages に追加してください。このアダプターは実行時にパッケージルートを基準として自身のツールチェーンを解決するため、バンドルされたコピーにはパッケージルートがありません。

開発中に 5 つの失敗モードがそれぞれ診断に 1 ラウンドを要しますが、そのすべてがサイレントに失敗します。1 フレーム目はレンダリングされますが、何も動かず、エラーも出ません。これらは現在すべてパッケージ内で処理されるため、独自に設定して「親切に」再導入しないでください。worklet には Babel プラグインが必要です(esbuild は Babel を実行しないため、useAnimatedStyle(() => …) は一度も呼び出されないクロージャになります)。プラグインのプリセットは api.assertVersion(7) を呼び出すため、Babel 8 では誤解を招くエラーになります。ネイティブのランタイムがバンドルされないように、.web.js は .js より先に解決される必要があります。バンドルより前にクロックの shim をインストールする必要があります。そして、page.setContent() は addInitScript を実行しません。実行するのはナビゲーションだけです。そのため、ページをディスクに書き込み、goto() で開きます。

デバッグするのではなく、ユーザーに伝えるべきことが 2 つあります。レンダリングにはブラウザーが必要です(glove-motion-doctor が行ごとの修正方法を表示します)。また、独自コーデックを含めずにビルドされた Chromium は、playwright-core install が配置するものも含めて、H.264 をデコードできません。その場合、コントロールは動作しますが、エラーのない黒い矩形が表示されます。mp4 に問題はなく、Chrome/Edge/Safari/Firefox では再生できます。コーデックに依存しない Chromium 向けには .webm をレンダリングしてください。

モデルを実際に成功させるもの

3 つのオープンモデルで 90 回実行して測定したところ、推測したインポートが最大の失敗要因でした。そのため /skills/imports.md が用意されており、実行を消費する前の書き込み時に、環境が誤ったインポート名を修正します。まずシステムプロンプトで /skills/README.md を参照するようにしてください。ファイルを読むのではなく、describe するようモデルに指示してください。80 ページの PDF は約 200KB のテキストに相当しますが、レスポンスの上限は 8KB です。簡略化したラッパーより、ライブラリ本来の API を公開する方が効果的でした。実際の pptxgenjs の機能を必要とするシナリオは、スイートで最高スコアを記録しました。

関連ファイル

API の詳細なリファレンスについては、api-reference.md を参照してください。 実際の実装例については、examples.md を参照してください。

よくある注意点

  1. model_response_complete と model_response:ストリーミングアダプターは model_response ではなく model_response_complete を発行します。購読側は両方に対応する必要があります。
  2. React フックでのクロージャキャプチャ:セッションのキーを変更するときは、古いクロージャを避けるために可変の let currentKey = key を使用してください。
  3. React の useEffect のタイミング:状態の更新は同じレンダーサイクル内では反映されません。早期リターンでガードしてください。
  4. ブラウザーで安全なインポート:glove-core はブラウザーで安全に使えるようになりました(ネイティブ依存関係はありません)。ネイティブ依存関係の better-sqlite3 を含む SqliteStore は、サーバー側専用の別パッケージ glove-sqlite にあります。サブパスインポート(glove-core/core、glove-core/glove など)は引き続き使えますが、ブラウザーで安全に使うために必要ではなくなりました。
  5. Displaymanager の大文字・小文字:具象クラスは DisplayManager ではなく Displaymanager(「m」は小文字)です。次のようにインポートしてください:import { Displaymanager } from "glove-core/display-manager"。
  6. createAdapter のストリームのデフォルト値:stream のデフォルト値は false ではなく true です。同期レスポンスが必要な場合は、明示的に stream: false を渡してください。
  7. ツールの戻り値:do 関数は { status, data, renderData? } を含む ToolResultData を返す必要があります。data は AI に渡され、renderData はクライアント側だけで使われます。
  8. Zod の .describe():スキーマのフィールドには必ず .describe() を追加してください。AI はこの説明を読み、何を入力すればよいかを理解します。
  9. displayPropsSchema は省略可能ですが、推奨されます:defineTool の displayPropsSchema は省略可能ですが、表示 UI があるツールでは推奨されます。表示を伴わないツールでは、代わりに生の ToolConfig を使用してください。
  10. モデルアダプターは renderData を除外します:モデルアダプターは、AI 向けにツール結果を整形する際に renderData を明示的に除外します。そのため、クライアント側だけのデータを安全に格納できます。
  11. SileroVAD は動的インポートが必要です:Next.js/SSR では、モジュールのトップレベルで glove-voice/silero-vad をインポートしないでください。WASM がサーバーバンドルに取り込まれないよう、await import("glove-voice/silero-vad") を使用してください。
  12. Next.js の transpilePackages:Next.js が ES モジュールを処理できるよう、next.config.ts の transpilePackages に "glove-voice" を追加してください。
  13. createTTS はファクトリーである必要があります:GloveVoice はターンごとに一度 createTTS を呼び出し、新しい TTS アダプターを取得します。単一のインスタンスではなく、() => new ElevenLabsTTSAdapter(...) を渡してください。
  14. 割り込み保護には unAbortable が必要です:pushAndWait のリゾルバーは、トリガーの段階で音声割り込みを抑制します(resolverStore.size > 0 のとき、GloveVoice は interrupt() をスキップします)。ただし、それだけではツールを保護できません。他の手段で interrupt() が呼び出された場合、中断シグナルがあってもツールを最後まで実行することを保証するのは、ツールの unAbortable: true だけです。チェックアウトのように変更が重要なツールには、両方を併用してください。音声優先のツールには pushAndForget を使用してください。
  15. 空の確定済み文字起こし:ElevenLabs Scribe は、短い発話に対して空の確定済み文字起こしを返すことがあります。アダプターは自動的に最後の部分文字起こしにフォールバックします。
  16. TTS のアイドルタイムアウト:ElevenLabs TTS の WebSocket は、アイドル状態が約 20 秒続くと切断されます。GloveVoice は各 model_response_complete の後に TTS を閉じ、次の text_delta で新しいセッションを開始することでこれに対応します。
  17. onnxruntime-web のビルド警告:onnxruntime-web による Critical dependency: require function is used in a way... の警告は想定内で、問題ありません。
  18. 音声のサンプルレート:すべてのアダプターで 16kHz モノラル PCM(デフォルト)を使用してください。プロバイダーから別の指定がある場合を除き、変更しないでください。
  19. narrate() はマイクを自動ミュートします:TTS 音声が STT/VAD にフィードバックされないよう、voice.narrate() は再生中にマイクを自動でミュートします。完了すると、以前のミュート状態に戻します。
  20. narrate() の前にパイプラインの開始が必要です:voice.start() の前に narrate() を呼び出すとエラーになります。TTS ファクトリーと AudioPlayer を初期化しておく必要があります。
  21. コンパクション中は音声が自動的に無音になります:コンテキストのコンパクションが開始されると、音声パイプラインは compaction_start から compaction_end までのすべての text_delta イベントを無視します。コンパクションの要約が読み上げられることはありません。
  22. React UI のフィードバックには isCompacting を使用します:コンパクションの進行中、GloveState.isCompacting は true になります。コンパクション中にローディングインジケーターを表示したり、入力を無効にしたりするために使用してください。
  23. <Render> はデフォルトの入力フォームを同梱しています:独自の入力フォームがある場合は、組み込みフォームを抑制するため、必ず renderInput={() => null} を渡してください。渡さないと入力フォームが重複します。
  24. ツールは React の外で実行されます:ツールの do() 関数はコンポーネントツリーの外で実行されます。React コンテキスト(例:useWallet())にアクセスするには、React コンポーネントから同期する可変のシングルトン参照を使用してください(ブリッジパターン)。
  25. 手動モードでは SileroVAD は不要です:turnMode: "manual"(プッシュ・ツー・トーク)を使う場合は、SileroVAD のインポートと、それに伴う WASM の負荷を省略してください。VAD が必要なのは turnMode: "vad" の場合だけです。
  26. システムプロンプトでツールを明示的に説明します:ツールには説明とスキーマがありますが、すべてのツールとそのパラメーターをシステムプロンプトに列挙すると、ツール選択の精度が大幅に向上します。
  27. Inbox 項目にはリモートストアの接続が必要です:createRemoteStore を使用する場合、getInboxItems/addInboxItem/updateInboxItem/getResolvedInboxItems の各アクションを指定しないと、Inbox はメモリ内ストアにフォールバックします。項目は再読み込み時に消失します。
  28. Inbox の解決済み項目はプレーンテキストメッセージです:解決済みの Inbox 項目はツール結果ではなく、ユーザーのテキストメッセージとして挿入されます。これにより、対応する tool_use/tool_result のペアがないことによる Anthropic API の検証エラーを回避できます。
  29. ブロッキング Inbox リマインダーは一時的です:保留中のブロッキング項目のリマインダーはプロンプトに含まれますが、ストアには保存されません。これにより、ターンをまたいだコンテキストの肥大化を防ぎます。
  30. MCP ツール名には __ を使います:ブリッジされた MCP ツール名は ${entry.id}__${tool.name} です。区切り文字の __(MCP_NAMESPACE_SEP)は、すべてのモデルプロバイダーで正規表現に安全です。Notion の search ツールは notion__search として公開されます。
  31. auth_expired は例外ではなく、契約です:MCP の callTool 中に発生した 401 形式のエラーは、{ status: "error", message: "auth_expired" } になります。フレームワークがトークンを更新することはありません。アプリ側でトークンを更新してストアに書き戻せば、次の呼び出しで getAccessToken 経由で取得されます。
  32. McpAdapter.deactivate は v1 ではツールをアンロードしません:永続化された状態は切り替わりますが、ブリッジされたツールはセッションが更新されるまで実行中のエージェントにロードされたままです。それを考慮して計画してください。
  33. mountMcp は失敗時も処理を継続します:アクティブなサーバーの再読み込みに失敗した場合(一時的な停止、期限切れのトークンなど)、失敗は console.warn で記録され、エージェントは残りのカタログで処理を続行します。mountMcp が例外をスローすることを前提にしないでください。
  34. serverMode は検出ポリシーのデフォルトを決定します:serverMode: true → auto-pick-best となり、ブリッジされたツールは権限確認を要求しません。serverMode: false(デフォルト)→ interactive ポリシーとなり、読み書き可能な MCP ツールには権限が必要です。上書きするには ambiguityPolicy を明示的に渡してください。
  35. インタラクティブ検出には mcp_picker レンダラーが必要です:interactive 曖昧性ポリシーは、displayManager 上の mcp_picker レンダラーを通じて表示されます。ブラウザー上でこのポリシーを使う場合は、そのレンダラーを登録してください。登録しないと pushAndWait が停止したままになります。
  36. 解析されるのは / ディレクティブのみです:parseTokens が検索するのは /name だけです。@name トークンはそのままモデルに渡されます。/usr/local のようなパスはそのまま残ります(usr はどのレジストリにも存在しません)。[email protected] のようなメールアドレスは一切変更されません。正当なユーザーメッセージに /compact が含まれ、その名前のフックがある場合は、実行されます。通常の文章と衝突しないフック名やスキル名を選んでください。
  37. 紐付けられた / ディレクティブは削除ではなくプレースホルダーに置き換えられます:/compact は、モデルが見る解析後のテキストでは [invoked_extension__hook_compact] になります。このプレースホルダーはディレクティブの正規表現に一致しないため、同じテキストを再解析してもエクステンションは再実行されません。フックとスキルのハンドラーが受け取る parsedText には、単独のディレクティブではなくプレースホルダーが含まれます。一致判定を行う際はこの点に注意してください。
  38. スキルによって挿入されたメッセージは is_skill_injection: true です:/skill の呼び出しで生成された合成ユーザーメッセージには、このフラグが設定されます。トランスクリプトのレンダラーでは、実際のユーザーターンと区別するために使用してください。他のメッセージと同様にストアに永続化され、コンパクション後も保持されます(他のすべてと同様に splitAtLastCompaction の対象です)。
  39. glove_invoke_skill は呼び出しごとに最新のレジストリを参照します:自動登録されるこのツールは実行時に this.skills を確認するため、build() の後に exposeToAgent: true を指定して定義したスキルもすぐに呼び出せます。新しい公開対象スキルが登録されるとツールの説明もその場で再構築されるため、モデルに表示される一覧は常に最新の状態です。同じことが glove_invoke_subagent とサブエージェントのレジストリにも当てはまります。
  40. @mention はモデル側のルーティングシグナルであり、解析されるディレクティブではありません:Claude Code のサブエージェント規約に従い、glove は @name トークンを解析しません。ユーザーメッセージ全体がモデルに渡され、モデルはツールの説明に基づいて glove_invoke_subagent を呼び出すかどうかを決めます。そのため、呼び出しは保証されません(モデルが @mention を無視することがあります)が、1 つのメッセージ内に複数の @mention があってもそのまま機能します。モデルはサブエージェントごとにディスパッチツールを呼び出せます。
  41. サブエージェントには親のコンテキストが共有されません:サブエージェントは独立して実行され、受け取る入力はエージェントが渡した prompt 文字列だけです。サブエージェントにコンテキストが必要な場合は、親エージェントがプロンプトに含める必要があります(Claude Code 方式)。ファクトリーは独自のストアを持つ新しい子 Glove を構築します。継承させたい場合は parentControls.glove.model と parentControls.displayManager を渡してください。
  42. subagent_invoked と subagent_completed は必ず対称に発行されます:ディスパッチャーではなく Executor が、すべての glove_invoke_subagent ツール呼び出しの前後で両方のイベントを発行します。親エージェントによる中断でディスパッチャーの Promise チェーンが短絡しても、Executor の中断分岐は status: "error" と message: "Subagent run aborted by the user." を含む subagent_completed を発行します。購読側は、両イベントが常に 1 対 1 で対応すると想定できます。
  43. フックの shortCircuit はユーザーメッセージを引き続き保存します:フックによってターンが短絡されても、トランスクリプトの一貫性を保つため、ユーザーのメッセージ(書き換え後)が最初にコンテキストへ追加されます。そのターンではモデルが呼び出されないだけです。
  44. トークン消費イベント:Observer は各モデルターンの後、購読側に token_consumption({ consumption: { tokens_in, tokens_out } })を発行します。StoreAdapter.addTokens は同じ TokenConsumptionCounter の形式を受け取ります。getTokenCount() は引き続き合計値を 1 つだけ返します。
  45. 推論の取得はオプトインです:OpenAICompatAdapter はデフォルトで reasoning_content を無視します。推論のトレースを Message.reasoning_content に保存するには、createAdapter または new OpenAICompatAdapter に reasoning: true(またはオブジェクト)を渡してください。MiMo アダプターは独自の方針で、常に取得します。
  46. reasoning_content フィールドと reasoning フィールド:アダプターはレスポンスのどちらのフィールドも読み取ります。DeepSeek/Qwen3/GLM/Kimi/MiniMax/MiMo は reasoning_content を出力し、OpenRouter は reasoning を出力します(reasoning_content は公式ドキュメントに記載された別名です)。取得された文字列は常に、Glove の標準フィールドである Message.reasoning_content に格納されます。
  47. ツール結果の要約はデフォルトで無効です:GloveConfig の enableToolResultSummary のデフォルト値は false です。これを true にするだけでは何も起こりません。短縮したい各ツールに generateToolSummary を追加し、さらに do() から generateSummaryArgs を返す必要があります。Executor が result.summary を設定するのは、ハンドラーと引数の両方が存在する場合だけです。
  48. summary が data を置き換えるのは古いコンテキスト内だけです:PromptMachine.summarizeOlderToolResults は最新のツール以外のユーザーメッセージを探し、そのインデックス以前のツール結果だけを書き換えます。現在のターンのツール結果は、常に完全な data とともにモデルに渡されます。ストアでは、すべての結果の data と summary がどちらもそのまま保持されます。書き換えられるのはモデルアダプターに渡されるメッセージだけです。
  49. summarizeOlderToolResults は空の要約をスキップします:置き換えが行われるのは result.summary が truthy の場合だけです。一部のツールだけが計装されたツールカタログでも安全です。計装されたツールは古いコンテキスト内で短縮され、計装されていないツールは元の data を保持します。summary を設定しない以外に、特定のツールを置き換えの対象外にする方法はありません。
  50. 文字列のエラーデータが二重に JSON 文字列化されることはなくなりました:すべてのモデルアダプター(anthropic、bedrock、mimo、openai-compat、openrouter)は、エラー結果のデータを JSON.stringify する前に typeof data === "string" を確認するようになりました。ツールが { status: "error", data: "some message" } を返した場合、モデルには Error: ...\nsome message と表示され、Error: ...\n"some message" とは表示されません。バグ修正のため、利用側での対応は不要ですが、モデル側でエラー文字列を解析している場合は動作の変更に注意してください。
  51. DeepSeek V4/MiMo では、ツールを使うターンで Echo が必要です:reasoning が有効な場合、アダプターは tool_calls を生成したアシスタントターンで Message.reasoning_content を Echo します。そうしないと DeepSeek V4 と MiMo はリクエストを拒否します。旧モデルの DeepSeek-R1 はこのフィールドを完全に拒否します。R1 を対象にする場合は reasoning: { echo: false } を設定してください。
  52. reasoning_effort の "minimal" は GPT-5 専用です:effort の全列挙値は "minimal" | "low" | "medium" | "high" ですが、"minimal" が使えるのは GPT-5/o-series だけです。MiMo の分岐では、これが通知なく除外されます。他のプロバイダーでは拒否される場合があるため、使用前にプロバイダーのドキュメントを確認してください。
  53. 適応型推論モデルでは "low" によって思考が抑制されることがあります:mimo-v2.5-pro などの適応型モデルでは、effort: "low" または "medium" を渡すと、推論が制限されるのではなく抑制されることがあります。一貫して深い推論を行うには "high" を渡すか、モデルに判断させるため未設定のままにしてください。
  54. extraBody によるプロバイダー固有の推論オプション:Qwen3 dashscope の enable_thinking/thinking_budget や、その他の非標準リクエストフィールドには、reasoning: { extraBody: { ... } } を使用してください。フィールドはリクエスト本文にそのままマージされます。一般的な用途向けに、構造化オプション(effort、reasoningObject、thinking)も利用できます。
  55. 権限はツールだけでなく、(ツール、入力)の組み合わせに紐付きます:ゲート対象の呼び出しごとに Executor.checkPermission は、モデルから渡された入力とともに store.getPermission(name, input) を呼び出します。デフォルトの MemoryStore は permissionKey(name, input) → "${name}::${JSON.stringify(input ?? null)}" によって入力を完全一致させるため、入力が異なる呼び出しでは個別に確認が行われ、同じ入力の呼び出しではキャッシュされた判定が使われます。カスタムストアでは、input を無視または正規化することで、コマンドに対する正規表現やパスに対するプレフィックスなど、より柔軟な照合を実装できます。
  56. requiresPermission には関数を指定できます:boolean | ((input: I) => boolean) です。ゲート自体が入力に依存する場合は、関数形式を使用してください。たとえば、書き込みはゲートするが読み取りはゲートしない単一の bash ツールでは、requiresPermission: (i) => !i.cmd.startsWith("ls") とできます。false を返すと、その呼び出しではストアの検索を完全にスキップします。true を返すと、通常の getPermission(name, input) のフローが実行されます。
  57. アップグレード時の SqliteStore の移行:既存のデータベースでは、permissions テーブルに input_hash 列が追加されます(主キーは (session_id, tool_name, input_hash) になります)。既存の行は input_hash = '' のまま保持されます(これは実際の呼び出しには一致しないセンチネルです)。そのため、新しい完全一致の仕様に合わせ、アップグレード後は各ツールで一度ずつ確認が求められます。glove-sqlite は引き続き非推奨です。glove-core の MemoryStore または BYO StoreAdapter を推奨します。
  58. mountMesh には Inbox 対応ストアが必要です:glove.store が 4 つの Inbox メソッド(getInboxItems、addInboxItem、updateInboxItem、getResolvedInboxItems)をすべて実装していない場合、MeshStoreUnsupportedError がスローされます。MemoryStore と SqliteStore はどちらも対応しています。BYO ストアでは明示的な対応が必要です。フレームワークが何もせずに処理を続けることはなくなりました。設定ミスがあれば即座に失敗します。
  59. Mesh のブロッキング送信は既存の Inbox 挿入経路を使います:glove_mesh_send_message({ blocking: true }) によって作成された保留中のブロッキング項目は、解決されるまで各ターンで Agent.buildPendingBlockingMessage を通じて一時的なリマインダーを生成します。確認応答または返信があると、解決済みの項目は標準の [Inbox: N item(s) resolved] 挿入を通じて表示されます。glove-core に新しい仕組みはありません。mesh は完全に追加型です。
  60. Mesh の送信者 ID は検証されません:MeshMessage.from は送信者が申告した値です。フレームワークは、送信者が登録済みの ID と一致するかを検証しません。認証済みメッセージが必要な場合は、adapter.send/broadcast の前にメッセージに署名し、subscribe ハンドラーで検証してください。これは、McpAdapter.getAccessToken が認証を利用側の責任としている方法と同様です。
  61. Mesh のタグプレフィックスは glove_mesh: ではなく mesh: です:ツール名は glove_mesh_*(フレームワークの glove_<package>_* 規則に準拠)ですが、Inbox タグには短い mesh:from:<sender>、mesh:broadcast:from:<sender>、mesh:waiting:<msg_id> のプレフィックスを使います。この違いは意図的です。タグはフィルターキー、ツール名はモデル向けです。ただし、Inbox の履歴を grep する際には注意してください。
  62. Mesh の返信は確認応答を兼ねます:in_reply_to: X を含む受信したダイレクトメッセージは、返信本文を解決済みの新しい Inbox 項目として表示すると同時に、X に対する保留中のブロッキング項目があれば解決します。返信する側が別途 glove_mesh_acknowledge を呼び出す必要はありません。
  63. InMemoryMeshAdapter はプロセス内でのみ有効です:プロセスごとに MeshNetwork を 1 つだけ構築し、すべての InMemoryMeshAdapter で共有してください。プロセスをまたぐ通信や分散メッセージングには、Redis pub/sub、NATS、HTTP Webhook などのトランスポート上に MeshAdapter を直接実装してください。確認応答のルーティング用に message_id → sender_id をマッピングする MeshNetwork の LRU は、デフォルトで 1024 件まで保持されます。非常に古いメッセージへの確認応答は、ベストエフォートです。
  64. Continuum の agent() ビルダーはモードによって分岐します:.input(zod) の後、.triggered() を呼び出すと TriggeredAgentBuilder<TInput> が返され、.concurrent() を呼び出すと ConcurrentAgentBuilder<TInput> が返されます。.retries(n)/.every("5m")/.withInput(default) は triggered でのみ使用できます。concurrent でこれらを使おうとすると、実行時エラーではなく型エラーになります。終端の .factory(ctx => Glove) は、AGENT_BRAND のブランドが付いたビルド済みエージェントを返します。これは station-signal の .run() と .step().build() の分岐に対応しています。
  65. .notify() は型レベルで concurrent 専用です:ConcurrentAgent<T> は .trigger(input) と .notify(input) の両方を公開し、どちらも kind: "notify" の実行を記録します。TriggeredAgent<T> が持つのは .trigger(input) だけで、これは kind: "trigger" の実行を記録します。実行時には実行種別によって処理が区別されます。trigger/定期実行は新しいサブプロセスを起動し、notify は稼働中のサブプロセスに IPC でルーティングされます。triggered エージェントで agent.notify() を呼び出すコードはコンパイルできません。
  66. triggered エージェントには .store(name => StoreAdapter) が必須です:これがない場合、起動のたびに新しい MemoryStore が作られ、前回までのコンテキストが失われます。これは Continuum の目的に反します。triggered エージェントに .store(...) が指定されていない場合、Discovery はコンソール警告を出します。concurrent エージェントはサブプロセスが長時間稼働するため、通常はストアなしでも問題ありませんが、ランナーが再起動する可能性がある場合は永続化すると役立ちます。
  67. Continuum のファクトリーで mountMesh をマウントすれば、そのまま動作します:mesh のマウント時点で、Continuum のストアは Inbox 対応である必要があります(mesh には 4 つの Inbox メソッドが必要です)。Glove の MemoryStore は対応しています。BYO StoreAdapter にも同じ Inbox メソッドが必要です。Continuum の基盤には mesh 専用の IPC はありません。ファクトリーで glove を構築した後に await mountMesh(glove, { adapter, identity }) を呼び出すだけで、アダプターはサブプロセス内で完全に実行されます。
  68. Continuum の購読側が受け取るのは onAgentEvent(envelope) であり、型ごとの Glove イベントではありません:ランナーは、子サブプロセスからのすべての Glove SubscriberEvent を、単一の AgentEventEnvelope({ agentName, runId, mode, event_type, data, timestamp })として転送します。特定の種類を処理するには、購読側で envelope.event_type を使って絞り込みます。notify の外で発行される常駐エージェントのイベント(ファクトリー設定中など)の場合、runId は null です。ラッパーでは、その上に型ごとのイベント振り分けを簡単に構築できます。
  69. Continuum の親が実行状態の唯一の情報源です:子は IPC エンベロープを発行するだけで、親ランナーがそれを adapter.updateRun(…) の呼び出しに変換します。子がアダプターと直接やり取りしたり、アダプターを再構築したりすることはありません(ステップをなくしたことで、station-signal よりも大幅に簡潔になっています)。終端 IPC で resolved フラグを設定し、アクティブ数を減らす処理は、await より前の同期的なクリティカルパスで行われます。そのため、終了猶予の 200ms と保留中の状態更新が重なっても、アダプターのバックエンドの遅延で二重に減算されることはありません。
  70. Continuum のウォーム再起動予算は、安定稼働が 60 秒続くとリセットされます:warmRestartPolicy.maxRestarts(デフォルト値は 5)は、エージェントのクラッシュループ再起動回数の上限です。ただし、ウォームサブプロセスが終了せずに 60 秒間 ready の状態を保つと、カウンターはゼロにリセットされます。そのため、数時間正常に動作した後にまれにクラッシュしても、エージェントが永久に使えなくなることはありません。頻繁にクラッシュする場合は、引き続き予算の上限に達します。リセットタイマーは終了ハンドラー内でクリアされるため、すぐに再クラッシュしても予算はリセットされません。
  71. Continuum ランナーの起動時環境変数ブロックリスト:NODE_OPTIONS、LD_PRELOAD、LD_LIBRARY_PATH、DYLD_INSERT_LIBRARIES は、サブプロセスに転送される親の環境変数と、エージェントが指定する .env({...}) の上書きの両方から除外されます。ローダーインジェクションに対する多層防御です。信頼モデルはあくまで「自分のエージェントコードを信頼する」ですが、影響の大きい抜け道を低コストで塞げます。spawn("node", …) が解決できるよう、PATH は意図的に保持されます。
  72. runner.notify(name, input) と agent.notify(input) の違い:どちらも kind: "notify" の実行を記録し、稼働中のサブプロセスにルーティングします。エージェントのインスタンスメソッドは getAdapter()(グローバルな configure({adapter}) シングルトン)を経由し、ランナーのメソッドはランナー自身のアダプターを経由します。1 つのプロセス内に複数のランナーがある場合(グローバルなアダプターでは競合します)や、どのランナーが実行を受け取るか明確にしたい場合は、ランナーのメソッドを使用してください。
  73. Continuum のブートストラップパスは本番環境では dist/bootstrap.js、開発環境ではフォールバックとして src/bootstrap.ts です:ランナーは existsSync を使い、存在する方を選びます。tsx で実行中に dist/ を削除しても、ソースファイルを使ってランナーは動作します。どちらも存在しない場合、実行時に spawn が ENOENT で失敗します。ランナーを起動する前に、パッケージをビルドするか、tsx で実行してください。
  74. tool_use_result に含まれるのは name/id ではなく tool_name/call_id です:ツールの呼び出しイベント(tool_use)には { id, name, input } が含まれます。結果には { call_id, tool_name, result } が含まれます。この 2 つのキーの組み合わせは同じではありません。誤ったキーで結果を呼び出しと照合すると、すべてのツールが処理中のまま解決されず、UI がいつまでも待ち続けます。
  75. 作業環境のパッケージはバンドラーの対象外にする必要があります:glove-working-environment とすべての glove-env-* を Next の serverExternalPackages に指定してください。ワーカープールは自身のモジュール URL を基準にエントリーポイントを解決します。バンドルされると、その URL はチャンクを指し、ワーカーファイルはその隣にありません。失敗するのはビルド時ではなく、最初の run_script 時です。env:motion では playwright-core も外部化する必要があります。自身のツールチェーンをパッケージルートから解決しますが、バンドルされたコピーにはそのルートがないためです。
  76. serverExternalPackages は pnpm ワークスペースのリンクを見落とすことがあります:Next は symlinks: true を固定で指定してパスを解決するため、解決後のパスに対して照合を行います。ワークスペースの依存関係は ../../packages/<name> に解決されます。node_modules のセグメントがないため一致せず、そのままバンドルされます。その場合は Can't resolve './worker-dev.mjs' が発生します。webpack の外部モジュールを明示的に追加してください。これはモノレポにのみ影響します。npm からインストールしたアプリでは問題ありません。
  77. 同期ライブラリには defineAdapter ではなく definePureModule を使用する必要があります:アダプターのバインディングはスレッドをまたぐため、非同期になります。アダプターを通じて await なしで sumBy(rows, 'n') を呼び出すと、文字列化された Promise が返され、実行は成功として報告されます。また、同期コールバック内(keys.map(k => camelCase(k)) など)では、正しいスペルで実装する方法がありません。definePureModule はワーカー内にパッケージをインポートして vm コンテキストにバインドするため、同期呼び出しが可能になります。
  78. Pure module の pick がサンドボックスの境界です:選択された関数は vm の外にあるワーカーのレルムで実行されます。文字列をコードとして扱うメンバーは決して選択しないでください。_.template は Function(source) でコードをコンパイルするため、サンドボックスの外で任意のコードが実行されます。プロトタイプのメンバーは定義時に拒否され、それ以外の名前は環境の作成時に実際のモジュールに存在するか検証されます。

コメント