AI からの操作¶
Overview¶
AI からの操作は、利用者がつないだ AI のエージェントに、プレイグラウンドのツールの操作を公開する機能である。
利用者は、ツールのページのスイッチで操作を許可し、エージェントが行った操作を、操作の記録の欄で見る。
アプリの構成とツールのページは プレイグラウンドの Design Doc にある。
方式と、許可と確認の形は、次の ADR が決めている。
- ADR-0044: AI のエージェントからの操作は、WebMCP の API で登録し、MCP-B の polyfill と relay でも呼べるようにする
- ADR-0045: AI のエージェントからの操作は、ツールごとに 7 日のあいだ保存するスイッチで許可し、消す操作と全体を置き換える操作はページが確認を出す
Scope¶
持つものは次のとおり。
- 23 個の操作の名前、入力、結果の形
- 許可のスイッチの位置と文言、スイッチの状態の保存
- 確認の画面の形と、確認を待つ間の動き
- 操作の記録の欄の位置と、1 行に出す内容
localStorageのキーと値、期限の扱い- 登録、許可、確認、記録の処理の流れと、コードの置き場
- ツールのページの、スイッチと記録の欄の位置、タイトル、ディレクトリの名前
持たないものは次のとおり。
- 操作が呼ぶ、ツールの中の処理。ツールごとの Design Doc が持つ。
- ツールのページのヘッダと、ペインの並べ方。ツールごとの Design Doc が持つ。この文書は、スイッチと操作の記録の欄を置く位置だけを決める。
SwitchとCheckboxの見た目とキーボードの操作。packages/ui/DESIGN.md が持つ。- 図とスライドを保存するデータの形。キャンバスの Design Doc が持つ。
Design¶
操作を公開するページ¶
操作を公開するツールは、Node の実行環境、SQL だけを動かすツール、Go を動かすツール、スライドのエディタ、Excalidraw のツール、draw.io のツールの 6 つである。
この文書では、6 つを node、sql、go、slides、excalidraw、drawio と呼ぶ。
- 操作を登録するのは、トップレベルで開いたページだけである(ADR-0045 の Decision 3)。
- 埋め込みかどうかは、
ai-operations/の React の外のモジュールが、window.self !== window.topで判定する。判定は、この 1 か所に置く。 - 埋め込みのときは、保存してあるスイッチの状態を読まず、スクリプトの読み込みと登録を行わない。ページはヘッダを出さないので、スイッチも出ない。
- 埋め込みかどうかは、
- Node の実行環境が、DB を使うプロジェクトで持つ DB には、SQL の操作を公開しない。エージェントは、
node_write_fileとnode_run_commandで、pgを使うコードを書いて実行する。
コードの置き場¶
ツールに共通の処理は、apps/playground/src/ai-operations/ に置く。
操作の定義は、ツールのディレクトリに、ツールごとに 1 つのファイルで置く。
| 置き場 | 持つもの |
|---|---|
apps/playground/src/ai-operations/ |
スイッチの状態と localStorage、polyfill と relay のスクリプトの読み込み、登録と解除、入力の検証、ファイルの 4 つの操作に共通の、入力の schema、相対パスの検証、書くときの大きさの上限、確認の順番と時間の上限、記録の store |
apps/playground/src/ai-operations/ の React |
スイッチ、確認の画面、操作の記録の欄のコンポーネント |
apps/playground/src/<ツール>/ |
そのツールの操作の定義(名前、説明、入力の schema、注釈、確認の前の検証、確認に出す内容、execute) |
@fukuemon/ui |
Switch、Checkbox、Dialog、Disclosure、StatusBadge、Notice、Button |
- ツールのディレクトリが、
ai-operations/を import する。ai-operations/は、ツールのディレクトリを import しない。 ai-operations/は、実行環境の package と、workspace/、project/を import しない。- ツールのディレクトリの、React の外のモジュールが、モジュールの初期化で、操作の定義の一覧とツールの名前を、
ai-operations/に渡す。React のコンポーネントは、渡さない。- Node の実行環境では、起動の Promise を持つ
src/node/のモジュールが渡す。 - SQL だけを動かすツールでは、入力欄の SQL を持つ
src/sql/のモジュールが渡す。 - Go を動かすツールでは、ファイルを持つ
src/go/のモジュールが渡す。 ai-operations/は、定義を受け取ったときに、保存してあるスイッチの状態を読み、オンなら登録する。
- Node の実行環境では、起動の Promise を持つ
sql-console/は、src/sql/を import しない。SQL を実行する関数と、実行中かどうかの値を、props で受ける。@fukuemon/sql-runtimeの、Web Worker で動かす実装は、実行を止める手段を持つ。止めるときは、Web Worker を作り直す。- 止める手段は、Web Worker の実装を作る関数が返す値にだけ持たせ、
SqlDatabaseの型には足さない。SqlDatabaseの型は、node-runtime/の標準入出力の実装と共有していて、その実装は止める手段を使わないためである。
- 止める手段は、Web Worker の実装を作る関数が返す値にだけ持たせ、
- 操作の定義のファイルは、状態を持つモジュールを import しない。2 つのファイルが互いを import しないようにするためである。
- 操作の定義のファイルは、定義の一覧を返す関数を export する。この関数は、今の状態を読む関数と、SQL や Go を実行する関数を、引数で受け取る。
- 状態を持つモジュールが、定義の一覧を返す関数を呼び、返った一覧を
ai-operations/に渡す。
- 操作の定義の
checkとexecuteは、呼ばれるたびに、React の外の値から、ツールの今の状態を読む。Context から読まない。- Node の実行環境では、起動の Promise を持つモジュールから、今の
NodeRuntimeのインスタンスを読む。実行環境を起動し直した後も、新しいインスタンスを使うためである。 - SQL だけを動かすツールでは、入力欄の SQL を持つ
src/sql/のモジュールから、今の SQL を読み、同じモジュールの SQL を実行する関数を呼ぶ。 - Go を動かすツールでは、ファイルを持つ
src/go/のモジュールから、今のファイルを読む。
- Node の実行環境では、起動の Promise を持つモジュールから、今の
- polyfill(
@mcp-b/global)と、relay につなぐスクリプト(@mcp-b/webmcp-local-relayが配るもの)は、apps/playgroundの依存に、範囲ではなく 1 つのバージョンで書く。読み込むのは、ai-operations/だけである。- 2 つは、ツールのページのスクリプトと別のファイルにして、
play.fukuemon.devから配る。
- 2 つは、ツールのページのスクリプトと別のファイルにして、
操作の定義は、次の型で書く。
type OperationDefinition<Input> = {
name: string;
description: string;
inputSchema: JsonSchema;
annotations?: {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
consequentialHint?: boolean;
};
check?: (input: Input) => Promise<OperationFailure | null>;
confirmation?: (input: Input) => ConfirmationContent;
execute: (input: Input) => Promise<OperationResult>;
};
type ConfirmationContent = {
title: string;
summary: string;
actionLabel: string;
detail?: string;
};
type OperationFailure = {
ok: false;
error: { code: ErrorCode; message: string };
} & Record<string, unknown>;
type OperationResult = ({ ok: true } & Record<string, unknown>) | OperationFailure;
type ErrorCode =
| "invalid_input"
| "not_found"
| "too_large"
| "not_ready"
| "declined"
| "timeout"
| "failed";
checkは、schema で表せない検証(パスの範囲、XML として読めるか、対象があるか、ツールが使えるか)を行う。確認の画面より前に呼ぶ。利用者が確定のボタンを押した後に、入力の誤りで失敗することを減らすためである。- ページは、
confirmationを持つ操作にだけ、確認の画面を出す。 - 説明の文(
description)は、日本語で書く。
操作の一覧¶
操作の名前は、ツールの名前を接頭辞にする。
relay を通すと、開いている全てのタブの操作が、MCP のクライアントの 1 つの一覧に並ぶ。
名前だけで、どのツールの操作かを読み取れるようにするためである。
| ツール | 名前 | 行うこと |
|---|---|---|
slides |
slides_read |
Markdown の全文、テーマ、枚数を返す |
slides |
slides_write |
Markdown の全文を置き換える |
slides |
slides_set_theme |
テーマを変える |
excalidraw |
excalidraw_read |
要素の一覧を返す |
excalidraw |
excalidraw_add_elements |
図形、文字、図形をつなぐ矢印を足す |
excalidraw |
excalidraw_update_element |
指定した要素の位置、大きさ、色を変える |
excalidraw |
excalidraw_clear |
全ての要素を消す |
drawio |
drawio_read |
図の XML を返す |
drawio |
drawio_add_cells |
図に mxCell を足す |
drawio |
drawio_write |
図の全体を、渡した XML に置き換える |
node |
node_list_files |
ファイルの一覧を返す |
node |
node_read_file |
ファイルの中身を返す |
node |
node_write_file |
ファイルを書く |
node |
node_delete_file |
ファイルを消す |
node |
node_run_command |
コマンドを実行し、終了コードと出力を返す |
sql |
sql_read_query |
入力欄の SQL を返す |
sql |
sql_run_query |
渡した SQL を実行し、結果を返す |
go |
go_list_files |
ファイルの一覧を返す |
go |
go_read_file |
ファイルの中身を返す |
go |
go_write_file |
ファイルを書く |
go |
go_delete_file |
ファイルを消す |
go |
go_run |
ファイルを実行し、終了コードと出力を返す |
go |
go_test |
テストを実行し、終了コードと出力を返す |
- 名前は、英小文字、数字、
_だけで書く。- polyfill(
@mcp-b/global6.2.0)は、[A-Za-z0-9_.-]の 1〜128 文字でない名前の登録を拒否する。 - relay(
@mcp-b/webmcp-local-relay6.2.0)は、名前を[a-zA-Z0-9_]に直して、MCP のクライアントに見せる。
- polyfill(
- 操作の注釈は、ADR-0044 の Decision 4 にある。ページが確認を出す操作は、ADR-0045 の Decision 4 にある。
結果の形¶
操作は、成功でも失敗でも、JSON の文字列を返す。
例外を投げない。
- 成功:
{"ok":true, ...}。okのほかの項目は、操作ごとに決まる。 - 失敗:
{"ok":false,"error":{"code":"<種類>","message":"<理由の文>"}}。
code は、次の 7 つ。
code |
返すとき |
|---|---|
invalid_input |
入力が schema に合わない。パスが作業ディレクトリの外を指す。XML を読めない。識別子が重なる |
not_found |
指したファイルか要素が無い |
too_large |
入力か、読むファイルが、上限を超える |
not_ready |
実行環境が起動していない。ツールの読み込みが終わっていない。開いている図かスライドが無い。SQL か Go の実行中である |
declined |
利用者が、確認の画面で断った。確認を待つ時間の上限を過ぎた。確認を待つ間に、スイッチがオフになった |
timeout |
コマンド、SQL、Go の実行が、時間の上限までに終わらなかった |
failed |
そのほかの失敗。SQL の失敗を含む。check と execute が投げた例外は、ai-operations/ が受けて、この値にする |
入力と結果¶
入力の schema は、操作の定義の inputSchema に 1 つだけ書く。
ai-operations/ が、inputSchema で入力を検証してから、check と execute を呼ぶ。
ブラウザの executeTool が、schema に合わない値をそのまま渡すためである。
入力は、次の型に当たる schema にする。
? の付いた項目は、省略できる。
どの入力も、型に無い項目を受け付けない(additionalProperties が false)。
型に無い項目があるときは、invalid_input を返す。
エージェントが、使われない項目を渡したことに気づけるようにするためである。
type Inputs = {
slides_read: {};
slides_write: { markdown: string };
slides_set_theme: { theme: string };
excalidraw_read: {};
excalidraw_add_elements: { elements: NewElement[] };
excalidraw_update_element: {
id: string;
x?: number;
y?: number;
width?: number;
height?: number;
strokeColor?: string;
backgroundColor?: string;
};
excalidraw_clear: {};
drawio_read: {};
drawio_add_cells: { cells: string };
drawio_write: { xml: string };
node_list_files: { path?: string };
node_read_file: { path: string };
node_write_file: { path: string; contents: string };
node_delete_file: { path: string };
node_run_command: { command: string; args?: string[]; timeoutMs?: number };
sql_read_query: {};
sql_run_query: { sql: string; timeoutMs?: number };
go_list_files: { path?: string };
go_read_file: { path: string };
go_write_file: { path: string; contents: string };
go_delete_file: { path: string };
go_run: { timeoutMs?: number };
go_test: { timeoutMs?: number };
};
type NewElement = {
type: "rectangle" | "ellipse" | "diamond" | "text" | "arrow";
x: number;
y: number;
id?: string;
width?: number;
height?: number;
text?: string;
from?: string;
to?: string;
};
成功のときの結果は、ok のほかに、次の項目を持つ。
| 名前 | 結果の項目 |
|---|---|
slides_read |
document、markdown、theme、slideCount |
slides_write |
slideCount |
slides_set_theme |
theme |
excalidraw_read |
document、elements |
excalidraw_add_elements |
ids(足した要素の識別子の配列) |
excalidraw_update_element |
id |
excalidraw_clear |
count(消した要素の数) |
drawio_read |
document、xml |
drawio_add_cells |
count(足した mxCell の数) |
drawio_write |
無い |
node_list_files |
entries({ path, kind } の配列)、truncated |
node_read_file |
path、contents |
node_write_file |
path |
node_delete_file |
path |
node_run_command |
exitCode、output、truncated |
sql_read_query |
sql |
sql_run_query |
results(文ごとの結果の配列)、ms |
go_list_files |
entries({ path, kind } の配列)、truncated |
go_read_file |
path、contents |
go_write_file |
path |
go_delete_file |
path |
go_run |
exitCode、output、truncated |
go_test |
exitCode、output、truncated |
documentは、ページで開いている図かスライドの識別子と名前である({ id: string, name: string })。slides_set_themeのthemeに渡せる値は、スライドのエディタが持つテーマの名前の一覧から、enumとして作る。この文書と操作の定義に、テーマの名前を書かない。excalidraw_readのelementsの要素は、id、type、x、y、width、height、text、strokeColor、backgroundColorを持つ。矢印は、つないだ図形の識別子のfromとtoも持つ。excalidraw_add_elementsは、次のとおり動く。elementsは、1 回に 200 個までである。超えるときは、too_largeを返す。- 矢印は、
fromとtoに図形の識別子を渡すと、図形をつなぐ。同じ呼び出しで足す図形のidも指せる。 idが、今の図の要素か、同じ呼び出しのほかの要素と重なるときは、invalid_inputを返す。fromとtoが指す図形が無いときは、not_foundを返す。
excalidraw_update_elementは、idの要素が無いときに、not_foundを返す。strokeColorとbackgroundColorは、#と 6 桁の 16 進の文字列である。backgroundColorは、transparentも受け付ける。drawio_add_cellsは、今の図にあるidと同じidのmxCellを渡されたときに、invalid_inputを返す。entriesのkindは、fileかdirectoryである。sql_run_queryのresultsの要素は、SqlResultと同じfields、rows、affectedRows、totalRows、truncatedを持つ。msは、実行にかかった時間である。markdown、xml、cells、contents、sql_run_queryのsqlは、UTF-8 で 1 MB までである。超えるときは、too_largeを返す。- 読み取りの結果のうち、大きさの上限を持つのは、ファイルの中身(
contents)、ファイルの一覧(entries)、コマンドと Go の実行のoutput、SQL の結果のrowsだけである。
操作の対象の図とスライド¶
slides、excalidraw、drawio の操作は、ページで開いている図かスライドだけを対象にする。
利用者が画面で見ているものと、エージェントが変えるものを、同じにするためである。
- 操作の入力に、図とスライドの識別子を持たせない。保存してある図とスライドの一覧を返す操作と、開く操作は持たない。
- 開いている図かスライドが無いときは、
not_readyを返す。 - 同じツールを 2 つのタブで開くと、relay は、操作の名前の後ろにタブの ID の短い文字列を付けて、両方を MCP のクライアントに見せる。エージェントは、次の 2 つで、タブを見分ける。
- 読み取りの結果の
document。 - relay が、操作の説明の先頭に付ける、ページのタイトル。
- 読み取りの結果の
実行環境の操作¶
node の操作は、NodeRuntime を使う。
| 名前 | 使うもの |
|---|---|
node_list_files |
files.tree の store の今の値 |
node_read_file |
files.read |
node_write_file |
files.write |
node_delete_file |
files.remove |
node_run_command |
processes.start |
- 実行環境が起動していないときと、起動に失敗したときは、どの操作も
not_readyを返す。 - パスは、作業ディレクトリからの相対パスだけを受け付ける。
..で外へ出るパスと絶対パスは、invalid_inputを返す。- ファイルの操作が届く範囲を、ファイルのツリーと保存の対象にそろえるためである。
- セキュリティの境界にはならない。
node_run_commandは、確認なしで、作業ディレクトリの外のファイルを読み書きできる。
node_list_filesは、pathの下の全ての階層を返す。pathを省くと、作業ディレクトリの全体を返す。保存しないファイルの規則のexcludeに当たるパスは返さない。1,000 件までにし、切り詰めたときはtruncatedをtrueにする。node_read_fileは、1 MB を超えるファイルにtoo_largeを、テキストとして読めないファイルにinvalid_inputを、無いファイルにnot_foundを返す。大きさとテキストの規則は、保存しないファイルの規則と同じである。node_write_fileは、ファイルがあれば上書きし、親のディレクトリが無ければ作る。上書きの前に、確認を出さない。node_delete_fileは、ファイルだけを消す。ディレクトリを指したときはinvalid_inputを、無いパスを指したときはnot_foundを返す。node_run_commandは、次のとおり動く。commandとargsを、そのままprocesses.startに渡す。コマンドの文字列を、引数に分ける処理は持たない。- プロセスの名前(
startのname)は、ai: <command>にする。プロセスは、端末のペインに、ほかのプロセスと同じように出る。 timeoutMsは、1,000 から 120,000 までの整数で、デフォルトは 30,000 である。上限までに終わらないときは、プロセスを終了させ、timeoutを返す。このときの結果は、errorのほかに、それまでのoutputとtruncatedを持つ。- MCP の SDK(
@modelcontextprotocol/sdk1.32.2)は、リクエストを待つ時間のデフォルトが 60 秒である。relay を通して呼ぶエージェントは、60 秒を超えるtimeoutMsを渡すと、結果を受け取る前に待つのをやめることがある。説明の文に、このことを書く。 - コマンドが 0 でない終了コードで終了したときは、
okをtrueにし、exitCodeにその値を入れる。 outputは、端末の制御の文字を除いた出力の、末尾の 100,000 文字である。切り詰めたときは、truncatedをtrueにする。
SQL だけを動かすツールの操作¶
sql の操作は、SQL だけを動かすツールの入力欄の SQL と、SqlDatabase を使う。
どちらの操作も、入力欄の SQL を書き換えない。
利用者が書いて保存した SQL が、エージェントの操作で消えないようにするためである。
- プロジェクトと、保存した SQL の、どちらかを読み終えていないときは、
sql_read_queryとsql_run_queryはnot_readyを返す。 sql_read_queryは、入力欄の今の SQL を返す。sql_run_queryは、入力のsqlを、src/sql/のモジュールの SQL を実行する関数に渡す。SQL を実行する関数は、画面の実行のボタンが呼ぶのと同じ、SqlDatabaseのrunを呼ぶ。- 確認を出さない。
DROPとDELETEを含む SQL も、確認なしで実行する。DB の中身は保存されず、Web Worker を作り直すと、初期の状態に戻るためである。 - 結果は、操作の記録の欄にだけ出す。SQL の画面の結果の表は、変えない。利用者が見ている結果が、利用者の操作なしに変わらないようにするためである。
- PGlite を読み込んでいないときは、読み込んでから実行する。読み込みの時間は、
timeoutMsに含めない。読み込めないときは、failedを返す。読み込みの間に来た呼び出しにも、実行中に来た呼び出しと同じく、not_readyを返す。 - 複数の文を渡したときは、
resultsに、文ごとの結果を入れる。 rowsは、エージェントのコンテキストを使い切らないよう、文ごとに、先頭の 200 行までにする。totalRowsに全体の行の数を入れ、切り詰めたときはtruncatedをtrueにする。200 行に切り詰める処理は、src/sql/の操作の定義が持つ。- SQL が失敗したときは、
failedを返す。このときの結果は、errorのほかに、Postgres のエラーコードのsqlStateと、エラーの位置のpositionを持つ。2 つは、errorの外に置く。どちらも、SqlResponseのerrorに無いときは、持たない。例は、{"ok":false,"error":{"code":"failed","message":"…"},"sqlState":"42P01","position":15}である。 - 複数の文のうち 1 つが失敗したときは、全ての文の変更を取り消す。
timeoutMsは、1,000 から 120,000 までの整数で、デフォルトは 30,000 である。上限までに終わらないときは、Web Worker を作り直して、timeoutを返す。DB の中身は、初期の状態に戻る。SQL の画面は、AI からの操作で DB を初期の状態に戻したことを出す。操作の説明の文に、時間の上限を過ぎると DB の中身が初期の状態に戻ることを書く。60 秒を超える値の扱いは、node_run_commandと同じである。- 実行中に、もう 1 つの
sql_run_queryが呼ばれたときは、後の呼び出しにnot_readyを返す。利用者がボタンで実行している間も、同じである。エージェントの操作で実行している間は、画面の実行のボタンを、実行中の表示にする。
- 確認を出さない。
- テーブルとカラムの一覧を返す操作は、持たない。エージェントは、
sql_run_queryでinformation_schemaを読む。
Go を動かすツールの操作¶
go の操作は、Go を動かすツールのファイルと、GoRunner を使う。
ファイルの 4 つの操作の入力と結果は、node の同じ名前の操作と同じである。
ファイルの 4 つの操作の入力の schema、相対パスの検証、書くときの contents の 1 MB の上限は、ai-operations/ に置き、src/node/ と src/go/ の操作の定義が使う。
読むときの、大きさの判定と、テキストとして読めるかの判定、パスが無いことの判定、ディレクトリかどうかの判定は、ツールの側(NodeRuntime と src/go/)に置く。
go_list_files、go_read_file、go_write_file、go_delete_fileは、src/go/の React の外のモジュールが持つファイルを読み書きする。エディタの表示と IndexedDB への保存は、利用者が書き換えたときと同じように更新される。- パスは、相対パスだけを受け付ける。
..で外へ出るパスと絶対パスは、invalid_inputを返す。ファイルは、UTF-8 で 1 MB までのテキストである。保存しないファイルの規則のexcludeは、使わない。 - Go を動かすツールは、ファイルをディレクトリに分けて持つ。ディレクトリを含むパスは、
nodeの操作と同じに扱う。go_list_filesは、pathの下の全ての階層を返す。1,000 件までにし、切り詰めたときはtruncatedをtrueにする。go_write_fileは、親のディレクトリが無ければ作る。go_delete_fileは、ファイルだけを消す。ディレクトリを指したときはinvalid_inputを、無いパスを指したときはnot_foundを返す。src/go/のモジュールは、空のディレクトリも保存する。go_list_filesは、空のディレクトリもentriesに入れる。
- プロジェクトと、保存したファイルの、どちらかを読み終えていないときは、
go_で始まるどの操作もnot_readyを返す。 go_write_fileは、ファイルがあれば上書きし、無ければ作る。上書きの前に、確認を出さない。go_delete_fileは、確認を出す。go_runは、src/go/のモジュールが持つ今のファイルを実行する。go_testは、同じファイルのテストを実行する。画面の実行のボタンとテストのボタンと、同じ関数を呼ぶ。go_runは、ルートのディレクトリの直下にあるmainの package を実行する。go_testは、全ての package のテストを実行する。- 入力でコードを受け取らない。エージェントは、先に
go_write_fileで書く。 - Yaegi の WASM を読み込んでいないときは、読み込んでから実行する。読み込みの時間は、
timeoutMsに含めない。読み込めないときは、failedを返す。読み込みの間に来た呼び出しにも、実行中に来た呼び出しと同じく、not_readyを返す。 - 実行を始めたときのファイルの写しを実行する。実行中に
go_write_fileとgo_delete_fileが呼ばれても、実行しているコードは変わらない。 - 出力は、出力のペインにも出す。利用者がボタンを押したときと、同じ表示にする。
timeoutMsは、1,000 から 120,000 までの整数で、デフォルトは 30,000 である。上限までに終わらないときは、GoRunnerの実行を止める手段で止めて、timeoutを返す。このときの結果は、errorのほかに、それまでのoutputとtruncatedを持つ。60 秒を超える値の扱いは、node_run_commandと同じである。- コンパイルのエラーと、テストの失敗は、
okをtrueにし、exitCodeを 1 にして、outputにエラーを入れる。コードがos.Exitを呼んだときは、渡した値をexitCodeにする。panic で終わったときは、exitCodeを 2 にする。go_testは、出力(=== RUN、--- PASS、--- FAIL)をそのまま返し、テストごとの結果に分けない。 outputは、末尾の 100,000 文字である。切り詰めたときは、truncatedをtrueにする。切り詰める処理は、src/go/の操作の定義が持つ。- 実行中に、もう 1 つの
go_runかgo_testが呼ばれたときは、後の呼び出しにnot_readyを返す。利用者がボタンで実行している間も、同じである。エージェントの操作で実行している間は、画面の実行のボタンとテストのボタンを、実行中の表示にする。
go_runとgo_testは、確認を出さない(ADR-0045 の Decision 4)。確認を出さない前提は、GoRunnerが Yaegi に渡す symbol を絞り、Yaegi で動かすコードが、ページのlocalStorageと IndexedDB を読み書きできないことである。- Yaegi に渡す symbol を
stdlib.Symbolsだけにすると、Yaegi で動かすコードは、syscall/js、syscall、unsafeを import できない。確認の結果は、ADR-0037 の Verification にある。 GoRunnerは、Yaegi を Web Worker の中で動かす。- Yaegi で動かすコードは、
net/httpを import できる。リクエストの扱いは、ADR-0064 が決めている。
- Yaegi に渡す symbol を
許可のスイッチ¶
スイッチは、ツールのページのヘッダの右端に置く。
- コンポーネントは、
@fukuemon/uiのSwitchを使う。ラベルは「AI からの操作」である。 - スイッチのほかに、オンかオフかを示す文言を、画面に足さない。操作の記録の欄は、ADR-0045 の Decision 5 が決めた欄で、状態を示す文言に当たらない。
- デフォルトはオフである。
- オンにすると、polyfill と、relay につなぐスクリプトを読み込み、操作を登録し、操作の記録の欄を出す。
- オフに戻すと、登録を解除し、出ている確認の画面を閉じ、操作の記録の欄を隠す。
- 確認を待っている呼び出しと、順番を待っている呼び出しには、
declinedを返す。 - 実行が始まっている操作は、止めない。結果は、記録の store に足す。
- Node の実行環境で、「AI からの操作」のタブを選んでいたときは、端末のタブを出す。選択の状態は書き換えず、描画のときに、選択が「AI からの操作」でスイッチがオフなら、端末のタブを出す。期限と、ほかのタブの操作でオフになったときも、同じように動く。
- 確認を待っている呼び出しと、順番を待っている呼び出しには、
- スクリプトを読み込めないときと、登録が拒否されたときは、スイッチをオフに戻し、
Notice(danger、isAlert)を、スイッチの下に出す。題は「AI からの操作を始められません」、本文は理由である。保存してあったオンで、ページを開いたときに失敗したときも、同じである。
登録から実行まで¶
次の図は、利用者がスイッチをオンにしてから、エージェントが呼んだ操作の結果が返るまでの流れを示す。
sequenceDiagram
participant U as 利用者
participant P as ツールのページ
participant A as ai-operations
participant M as document.modelContext
participant G as AI のエージェント
U->>P: スイッチをオンにする
P->>A: 操作の定義の一覧を渡して、有効にする
A->>A: localStorage に期限を書く
A->>A: polyfill と relay のスクリプトを読み込む
loop 操作ごと
A->>M: registerTool(AbortSignal を渡す)
end
G->>M: 操作を呼ぶ
M->>A: 入力
A->>A: スイッチの期限を見る
A->>A: inputSchema で検証する
A->>P: 操作の定義の check を呼ぶ
P-->>A: 失敗、または無し
alt schema に合わない、または check が失敗を返した
A-->>G: 失敗の JSON の文字列
else 通った
opt 確認を出す操作で、「このページでは今後尋ねない」を選んでいない
A->>U: 確認の画面を出す
U-->>A: 確定のボタンか、「やめる」を押す
end
alt 利用者が断った、または待つ時間の上限を過ぎた
A-->>G: declined
else
A->>P: 操作の定義の execute を呼ぶ
P-->>A: 結果
A-->>G: 結果の JSON の文字列
end
end
A->>A: 記録の store に 1 件足す
- 登録の解除は、
registerToolに渡したAbortSignalで行う。 - スイッチのオンを保存してあるページでは、ツールのモジュールが定義を渡したときに、同じ流れで登録する。
- 呼び出しが届いたときに、スイッチの期限を過ぎていれば、スイッチをオフにし、
declinedを返す。バックグラウンドのタブでは、タイマーが遅れることがあるためである。 - 記録の store に 1 件を足すのは、結果が決まったときである。失敗と
declinedで終わった呼び出しも足す。実行中の呼び出しと、確認を待っている呼び出しは、欄に出さない。 - React のコンポーネントは、Effect の中で、スクリプトの読み込みと登録を行わない。
ai-operations/の、React の外のモジュールが進め、コンポーネントは、状態をuseSyncExternalStoreで読む。 - polyfill は、ブラウザが
document.modelContextを持つときも読み込む(ADR-0044 の Decision 3)。
確認の画面¶
確認を出す 5 つの操作は、利用者が確定のボタンを押してから実行する。
確認の画面は、@fukuemon/ui の Dialog で組む。
ConfirmDialog は、チェックボックスを置けないので使わない。
題、本文、ボタンの文、フォーカスは、ConfirmDialog の規則にそろえる。
-
題(
title)と、確定のボタンの文(actionLabel)は、操作ごとに次のとおりにする。名前 題 確定のボタン slides_writeスライド「<名前>」の全文を置き換える 置き換える excalidraw_clear図「<名前>」の全ての要素を削除する 削除する drawio_write図「<名前>」の全体を置き換える 置き換える node_delete_fileファイル「<パス>」を削除する 削除する go_delete_fileファイル「<パス>」を削除する 削除する
- 本文に、AI のエージェントが呼んだ操作であること、操作の名前、何が起きるかの文(
summary)を書く。excalidraw_clearのsummaryは、消す要素の数を含む。 - 本文に、元に戻せないことを書く。「元に戻す」でエージェントの操作を戻せるツールでは、本文を、「元に戻す」で戻せるという文に替える。
- 渡された内容の全文(
detail。slides_writeのmarkdownと、drawio_writeのxml)は、Disclosureを開くと読める。 - 本文の下に、
Checkboxの「このページでは今後尋ねない」を置く。デフォルトは、チェックなしである。 - ボタンは、「やめる」(
secondary)と、確定のボタン(danger)の 2 つである。開いたときのフォーカスは、「やめる」に当てる。 - 確定のボタンを押すと、実行が終わるまで、確定のボタンを読み込み中にし、「やめる」、Esc のキー、背景のクリックで閉じない。実行が終わると、閉じる。
- 「このページでは今後尋ねない」をチェックして確定のボタンを押すと、そのツールでは、その後の操作を、確認を出さずに実行する。「やめる」を押したときは、チェックを保存しない。
確認を待つ間は、次のとおり動く。
- 「やめる」、Esc のキー、背景のクリックは、
declinedを返す。 - 確認が出ている間に、確認を出す操作がもう 1 つ呼ばれたときは、届いた順に、1 つずつ確認を出す。確認を出さない操作は、待たせずに実行する。
- 呼び出しが届いてから 50 秒で、
declinedを返す。確認が出ていれば、閉じる。順番を待っている間に 50 秒が過ぎた呼び出しは、確認を出さずにdeclinedを返す。- エージェントが待つのをやめた後に、利用者が確定のボタンを押して操作が実行され、その結果を受け取るエージェントがいない、ということを防ぐためである。
- 50 秒は、MCP の SDK がリクエストを待つ時間のデフォルトの 60 秒より、短くした値である。
- 確認の画面は、ページがバックグラウンドのタブにあるときも開く。
操作の記録の欄¶
操作の記録の欄は、スイッチがオンの間だけ出す。
- Node の実行環境では、下のペインのタブ(端末、コンソール、HTTP の一覧と同じ並び)に、「AI からの操作」のタブを足す。
- SQL だけを動かすツール、Go を動かすツール、スライドのエディタ、Excalidraw のツール、draw.io のツールでは、ページの下端に、開閉できる欄を置く。閉じている間は、題の「AI からの操作」と、最後の 1 行だけを出す。
- 欄の先頭に、
Disclosureの「AI のエージェントのつなぎ方」を置く。relay を起動するときに渡す--widget-origin <ページの origin>を書く。本番では、https://play.fukuemon.devになる。
1 行は、1 回の呼び出しである。
- 1 行に、時刻(時、分、秒)、操作の名前、引数の要約、結果の
StatusBadge、結果の要約を出す。- 要約は、先頭の 80 文字である。
StatusBadgeは、成功(success)、失敗(danger)、中止(neutral)の 3 つで、文字でも状態を出す。okがtrueの結果は成功、declinedは中止、ほかのcodeは失敗である。
- 行を開くと、引数と結果の全文を読める。それぞれ、10,000 文字までを出す。
- 新しい行は、下に足す。
- 引数と結果は、文字として出す。HTML として描かない。JSON の文字列の中の Markdown と XML は、エスケープを解除して出す。
- 欄は、
role="log"を持つ。
記録は、ai-operations/ の store が、メモリにだけ持つ。
- 200 件までを持ち、超えた古い記録を消す。
localStorageと IndexedDB に書かない。引数と結果が、ファイルの中身を含むためである。- ページを読み込み直すと、消える。スイッチをオフにしても、ページを開いている間は残り、オンに戻すと出る。
スイッチと確認の状態の保存¶
スイッチのオンと、「このページでは今後尋ねない」の状態は、ツールごとに、localStorage の 1 つのキーに保存する。
- キー:
fd-ai-operations:<ツール>。<ツール>は、node、sql、go、slides、excalidraw、drawioのどれかである。 - 値: 次の形の JSON。
type StoredPermission = {
v: 1;
enabledUntil: number;
skipConfirmUntil: number | null;
};
enabledUntilは、スイッチをオンにした時刻の 7 日後である。skipConfirmUntilは、「このページでは今後尋ねない」を選んだ時刻の 7 日後である。どちらも、エポックからのミリ秒で持つ。- ページを開いても、期限を延ばさない。
- キーが無いとき、値を JSON として読めないとき、値が型に合わないとき、
enabledUntilを過ぎているときは、スイッチをオフとして扱い、キーを消す。 skipConfirmUntilを過ぎているときは、確認を出す状態に戻す。- ページを開いている間に
enabledUntilが来たときは、その時点で、スイッチをオフにし、登録を解除する。 - スイッチをオフにしたときは、キーを消す。「このページでは今後尋ねない」の状態も消えるので、オンに戻すと、確認を出す状態から始まる。期限でオフになったときも、同じである。
- 「このページでは今後尋ねない」の状態が続くのは、選んでから 7 日と、スイッチがオフになるまでの、短い方である。
- 同じツールを複数のタブで開いているときは、
storageのイベントを受けて、ほかのタブが書いた値(スイッチと、「このページでは今後尋ねない」の両方)を、自分のタブにも反映する。 localStorageを読み書きできないときは、スイッチは、ページを開いている間だけ働き、状態を保存しない。
ツールのページの、スイッチと記録の欄の位置、タイトル、ディレクトリの名前¶
ツールのページは、この機能のために、次の位置と名前を持つ。
- ツールのページは、ヘッダの右端に、スイッチを置く場所を持つ。
- SQL だけを動かすツール、Go を動かすツール、スライドのエディタ、Excalidraw のツール、draw.io のツールのページは、下端に、操作の記録の欄を置く場所を持つ。
- Node の実行環境のページは、下のペインのタブの並びに、
ai-operations/のコンポーネントを置ける。選択の状態の、下のペインのタブの値に、「AI からの操作」のタブを持つ。 - ツールのページは、タイトル(
<title>)に、開いているものの名前を入れる。図とスライドは、その名前である。Node の実行環境、SQL だけを動かすツール、Go を動かすツールは、埋め込みの ID である。 - ツールのディレクトリの名前は、
node、sql、go、slides、excalidraw、drawioである。
Interface¶
置くファイル¶
apps/playground/src/ai-operations/: ツールに共通の処理と、スイッチ、確認の画面、操作の記録の欄のコンポーネント。apps/playground/src/<ツール>/: ツールごとの操作の定義のファイル。- polyfill と、relay につなぐスクリプト:
play.fukuemon.devから配る。スイッチがオンのときにだけ読み込む。
読む値¶
localStorageのfd-ai-operations:<ツール>: スイッチと、「このページでは今後尋ねない」の期限。
使うコンポーネント¶
@fukuemon/uiのSwitch: 許可のスイッチ。@fukuemon/uiのCheckbox: 確認の画面の「このページでは今後尋ねない」。@fukuemon/uiのDialog、Button、Disclosure、StatusBadge、Notice: 確認の画面と、操作の記録の欄。
動くチェック¶
- Oxlint の
no-restricted-imports:apps/playground/src/ai-operations/からの、ツールのディレクトリの import を止める。対象は、node/、slides/、excalidraw/、drawio/、sql/、go/である。