自分のしたことが見えていなかった — 空振りしたWarm-Aと、一本の文字列
インフルが治りかけてきた頃、最初にやったのはコンテキスト管理の実装だった。
設計メモの実装順の1番目。Warm-A、つまり古いツールの結果をプレースホルダーに置き換える処理。一番シンプルで、効果の数字も出ている。まずこれを動かして、手応えを掴むつもりだった。
AnthropicがAPI側で同じことをやってくれる機能(Context Editing API)も見つかっていたので、自前で書く前にそっちを試すことにした。公式にあるなら、そっちを使えばいい。
・・・動かなかった。
正確には、動いてはいるけど何も起きない。消すはずのツール結果が、1件も消えない。
コーディングしてくれているClaude Code(以降Code)が原因を追いかけた。まずログにエラーが1つ。直すと、今度は発火の条件が永遠に満たされない。条件を変えても発火しない。
何度か回り道をしたあとで、Codeが根っこを見つけてきた。MONET(僕が普段話しかけている、中心のAI)の会話をDBに保存する時、ツールの呼び出しと結果は捨てて、文章だけを残す作りになっていた。次のターンで会話を組み立て直す時、古いツールの結果はそもそも入っていない。
消す対象が、最初から無かった。
なんでDBへの保存がマスクの話に関係あるの、とCodeに聞いた。そこを掘ってもらって、ようやく絵が見えた。
今の作りは、ツールの結果が次のターンに持ち越されない。つまり「古いツール結果がコンテキストを汚す」という問題は、別の理由でとっくに防がれていた。Warm-Aでやろうとしていたことは、半分は空振りだった。
ただ、裏返すともう一つのことが見えてくる。
MONETは、自分がさっき何をしたのかを見られない。
ツールを呼んで結果を受け取る。そのターンの中では結果が見えている。でも次のターンになると、結果は消えている。残っているのは「完了にしました」という自分の文章だけ。
Codeと話していて、ここで一つつながった。MONETはときどき「あれ、私ちゃんとやりましたっけ?」と自信なさげになる。確認したはずのことを確認し直そうとする。あれは性格じゃなくて、見えていないからだった。
そこで出てきたのがforce-displayという案だった。
ツールの生の結果を、MONETの要約とは別に、アプリがチャット画面に直接出す。それを普通のメッセージとしてDBにも保存する。そうすれば次のターンで、MONETは「さっきHandler(ハンドラー。タスクとスケジュール管理アプリ)を呼んだら、こういう結果が返ってきた」を自分で読める。
僕の側にも効く。
前にID:42の件を書いた。FB対応のタスクを完了にしてと頼んだら、MONETが全然違うID:42を完了にして、「完了にしたよ」と返してきた。三重防御は全部すり抜けた。形式は正しかったから。あの時、僕はMONETの要約しか見ていなかった。要約が「完了にしたよ」なら、信じるしかない。
生の結果が画面に出ていれば、「ID:42過去画像動画化 → 完了」という行を見て、違う、と止められる。
次の段(パネルのラベル付け)より先に、force-displayをやることにした。
同じ日の次のセッションで、中身を詰めた。
Codeがコードを読んでいる間、僕が気になったのは3つだけだった。誰が結果を表示するのか。表示される生のテキストはどういう形なのか。画面に出す文字列と、MONETが受け取る結果は、別の文字列なのか。
最後の質問で方針が決まった。同じ文字列にする。
表示用を人間向けに整えて、MONET用は別に作る、という手もある。そのほうが自由は利く。でも2本あると、いつか片方だけ更新されてずれる。だから、Handlerが返す1本の文字列を、MONETにも、画面にも、DBにも、そのまま流す。
そうすると、その1本の質を上げるしかなくなる。Handlerの返事はこう変わった。
変更前:
タスクID:42を更新しました
変更後:
タスクID:42「買い物」[プライベート]: ステータス 今日 → 完了
ID、タイトル、プロジェクト、何が何から何に変わったか。人間が読んで一目でわかる形にしたら、それがそのままMONETにとっても良い材料になった。
実装はCodeがやった。
途中でいくつか引っかかっている。ツールの結果をMONETの発言として保存するとAPIに弾かれる。発言は「ユーザー → AI → ユーザー」と交互に並ばないといけないルールがあるので、ユーザー側の発言として保存した。最初はDBには入ったのに画面に何も出なかった。保存と画面への送信は別の処理で、送信を1行書き忘れていた。ツールを呼ぶ経路は3本あって、1本でも漏らすとその時だけ表示が消えるので、全部に入れた。
動かして、チャットに1行出た。
「タスクID:42「買い物」[プライベート]:ステータス 今日 → 完了」
MONETの要約の下に、生の結果がそのまま並ぶ。僕が見ている文字列と、MONETが受け取った結果と、DBの履歴が、同じ1本になった。
期待していることがもう一つある。結果がこれだけ具体的なら、MONET自身が「頼まれたタスクと、いま操作したタスクが違う」と気づけるかもしれない。追加のコードは書いていない。材料を渡しただけ。ここはまだ試していない。
この日、もう一つ決めたことがある。
アプリ側にAIを載せるかどうか、ずっと揺れていた。数日前には「Handlerにも判断役のAIを」という案も出ていた。でもWarm-Aの空振りを追いかけているうちに、考えが固まった。アプリはAI無しのまま、決まった通りに動けばいい。考えるのはMONETだけ。そのMONETがちゃんと見て考えられるように、コンテキストの側を整える。
1週間ほどして、この日の選択が一つ裏目に出た。
MONETが、僕が結果をくれた、という言い方をするようになった。自分でHandlerを呼んで受け取った結果を、僕が渡したものだと思っている。
ツールの結果は、ユーザー側の発言として保存していた。DBに残っているのは文章だけで、「MONETがツールを呼んだ」という事実は消えている。組み立て直された履歴の中では、結果は僕の発言として並んでいる。MONETから見れば、僕がくれたものに見える。当然だった。
その少し前のセッションで、Codeは直し方を3つ並べて「どれがいいですか」と聞いてきていた。ツールの呼び出しと結果を、構造ごとDBに残す案も、その3つのうちの1つだった。
僕は半日、何かおかしいと感じていた。3つは同じ重さの選択肢じゃない気がした。構造ごと残さない限り、MONETは自分がやったことと僕が言ったことを区別できない。それは選べる話じゃなくて、先にやっておかないといけない話だった。
自分のしたことが見えないと、AIは確認し直すか、作り話で埋める。force-displayで見えるようにはなった。でも、誰がしたのかまでは、まだ見えていなかった。