ある朝、Antigravity の Background Agent が前日の AdMob レビュー結果を完全に忘れていて、同じ警告を3回続けて私のメールボックスに投げてきました。Agent run のログを追いかけると、メモリは持っているはずなのに、復元先のキーが微妙にずれて読み出せていなかった、というのが原因でした。アーティスト・クリエイターの廣川政樹(Dolice)です。2014年から個人で iPhone / Android アプリを開発しながら、Antigravity を含むエディタ統合の AI エージェントで日々の運用を回しています。同じような「Agent が情報を覚えていない/逆に覚えすぎている」現象に何度かやられてから、私自身の中で「Agent が抱える情報は、寿命の長さで3階層に分けないと必ず壊れる」という結論にたどり着きました。
本稿はその3階層 — ephemeral / journal / canonical — の責任分界と、Cloudflare Workers の物理ストア(KV / Durable Objects / R2 / D1)への落とし込み方、そして write-back 境界の決め方を、累計5,000万DLの壁紙アプリ群を裏で支えてきた実運用ルールとセットでお渡しします。Background Agent / Sub-agent / Browser Sub-agent を複数本並走させているチーム(あるいは個人)にとって、設計判断を1段落ずつ詰める材料になればと思います。
同じ Antigravity Agent でも、覚えていてほしい情報には3つの寿命がある
Antigravity の Agent が触る情報を雑に「メモリ」と呼んでしまうと、必ずどこかで破綻します。私が実運用で痛い目を見てから採用したのは、以下の3階層に責任を分ける考え方です。
階層 寿命 失われたときの被害 例
ephemeral 1 run ほぼゼロ(再計算可能) 中間プロンプト・思考ログ・LLM 出力の生テキスト
journal 数日〜数週間 中(再現に時間がかかる) 意思決定ログ・実験結果・A/B 比較の中間スコア
canonical 永続 致命傷(サービス本体に直結) 公開済み記事スラッグ・ユーザー購入記録・課金状態
ポイントは「失われたときに何が起きるか」で寿命を決めることです。情報量や物理的なサイズではありません。たとえば LLM の長い思考ログは数十万トークンあっても ephemeral で十分ですし、たった 1 件のスラッグ重複検知ログは canonical に置かないと事故ります。
この3階層に分けると、Agent コード側は次の3つだけを意識すればよくなります。
その情報は、次の run で再計算できるか? → できるなら ephemeral
再計算できないが、最悪「やり直し」が許されるか? → 許されるなら journal
失えばユーザー体験そのものが壊れるか? → 壊れるなら canonical
これだけです。後述の通り、3層は別々の物理ストアにマッピングしますが、Agent 側コードからは統一的なインターフェースで扱えるようにアダプタを噛ませます。
ephemeral — 1回の Agent run 内だけで生きる作業メモリ
ephemeral は、Agent run の入口で生まれて出口で捨てられる情報です。中間プロンプト、LLM 応答の生テキスト、ファイル一覧の snapshot、検索結果、再計算の途中値、こういったものが該当します。失われても次の run で再生できるので、Cloudflare KV のような共有ストアに置く価値はほぼゼロです。
Antigravity の Background Agent では、私はこの層を Worker のローカルメモリ(つまり JavaScript の Map / Object)で完結させています。コードはこんな雛形です。
// state/ephemeral.ts
export class EphemeralStore {
private bucket = new Map < string , unknown >();
get < T = unknown >( key : string ) : T | undefined {
return this .bucket. get (key) as T | undefined ;
}
put < T = unknown >( key : string , value : T ) : void {
this .bucket. set (key, value);
}
// run 終了時に必ず呼ぶ。意図的に明示する。
dispose () : void {
this .bucket. clear ();
}
}
意図的に dispose を明示しているのは、ephemeral を「黙って捨ててもいい」ではなく「能動的に捨てる」ものとして扱いたいからです。Sub-agent から戻ってきた中間 LLM 応答をうっかり journal や canonical に書き込んでしまうと、ストレージコストが青天井になります。私の場合、Background Agent 1本あたり ephemeral だけで 200〜800KB / run を扱うので、月単位だと GB スケールになります。これを真面目に KV に書いていたら月の運用予算 ¥800 では絶対に収まりません。
実装上の落とし穴も一つ。Cloudflare Workers の同一 Isolate が再利用されると、ephemeral として作った Map が次の run まで残ってしまう挙動を踏みました。これは Worker のグローバル変数として保持していた私のミスで、対処として EphemeralStore は必ず run のエントリ関数内で new するように変更しました。本番運用に乗せる前に、Isolate 再利用を意図的に発火させるテストを書くことを推奨します。
journal — 数日〜数週間の意思決定ログ。再現可能だが必須ではない
journal は、再現はできるけれど再現に時間や費用がかかる情報を置く層です。Agent が下した意思決定の根拠ログ、A/B テストの中間スコア、Browser Sub-agent が観測した AdMob ダッシュボードの差分、過去の自動修復ログ、こういったものが該当します。
journal の判定基準は1つだけで、「失っても完全には困らないが、再現には半日以上かかる」かどうかです。私の場合、累計5,000万DLの壁紙アプリ群を 12 年支え続けた中で、journal が無いまま再現すべき失敗が複数回あり、そのたびに半日〜2日溶けました。今はこの層を真面目に持つようにしています。
物理マッピングとしては、Cloudflare KV のように「読み取り無料・書き込み従量・eventual consistency」の特性が journal にちょうど噛み合います。書き込み頻度が低く、読み取り頻度はそこそこあり、最新性は緩くてよいからです。
// state/journal.ts
export class JournalStore {
constructor ( private kv : KVNamespace , private agentId : string ) {}
private k ( scope : string , id : string ) : string {
// agent ごとに名前空間を切る。誤読み防止。
return `journal:${ this . agentId }:${ scope }:${ id }` ;
}
async append ( scope : string , entry : Record < string , unknown >) : Promise < void > {
const id = crypto. randomUUID ();
const value = JSON . stringify ({ at: Date. now (), ... entry });
// 30日の TTL を必ずつける。journal は永続ではない。
await this .kv. put ( this . k (scope, id), value, { expirationTtl: 60 * 60 * 24 * 30 });
}
async list ( scope : string , limit = 50 ) : Promise < Array < Record < string , unknown >>> {
const { keys } = await this .kv. list ({ prefix: this . k (scope, "" ), limit });
const items = await Promise . all (keys. map (( k ) => this .kv. get (k.name, "json" )));
return items. filter (Boolean) as Array < Record < string , unknown >>;
}
}
ここで2つ実運用の落とし穴を共有します。1つ目、journal のキーに必ず journal: の prefix を入れること。canonical と物理 KV を共有してしまっても、prefix で誤読み・誤上書きを防げます。2つ目、TTL を必ずつけること。journal は性質的に再現可能なので、30 日経って捨てても困りません。逆に TTL なしで運用すると、半年経った頃に Agent が「過去の意思決定が膨大すぎて文脈が壊れる」状態になります。私はこれを 2 度やっています。
journal の月額コストは、書き込み 100k / 月程度・読み取り 1M / 月程度の小規模 Agent 運用で、Cloudflare KV の課金枠だと ¥50 前後に収まります。これも 1 サイトあたり ¥800 の運用予算の中では十分許容範囲です。
canonical — 削れば致命傷になる、サービス本体のソース・オブ・トゥルース
canonical は失えば終わる情報です。記事のスラッグ、Stripe の購入レコード、premium 会員のメール、AdMob の収益サマリーの月次集計(再取得可能だが API 制限で時間がかかる)、こういったものが該当します。
私の運用ルールはシンプルで、canonical はAgent 自身ではなく、必ず外側のシステム側にソース・オブ・トゥルースを置くというものです。Agent はあくまでも canonical を「読む」か「書き戻す」かのどちらかしかしません。Agent 内部で canonical を「持っている」つもりになると、Agent 側のバグでサービス本体がぶっ壊れます。
物理マッピングは、データの構造によって2つを使い分けています。
データ型 物理ストア 理由
行指向・トランザクション必要 Cloudflare D1(または Stripe API) 整合性が要・SQL で監査クエリも回せる
ドキュメント指向・並行更新が必要 Cloudflare Durable Objects 強い順序保証・1キーあたり1インスタンス
大容量・読み取り中心 Cloudflare R2 月次集計の永続スナップショット用途
具体例で言うと、Antigravity Lab の記事公開済みスラッグ一覧は GitHub repo(つまり Git そのもの)が canonical で、Agent はそれを clone して読むだけ。書き込みは GitHub API 経由でしか発生させません。Agent 側の KV や DO は journal 扱いで、Git 側に書き戻したらすぐ捨てる、という分離を徹底しています。
Durable Object を使った canonical アダプタの最小例も載せておきます。premium 購入記録のような並行更新が起きうるものに向いています。
// state/canonical-do.ts
export class PurchaseRegistry {
state : DurableObjectState ;
constructor ( state : DurableObjectState ) { this .state = state; }
async fetch ( req : Request ) : Promise < Response > {
const url = new URL (req.url);
if (req.method === "POST" && url.pathname === "/grant" ) {
const { email , slug } = await req. json <{ email : string ; slug : string }>();
// 同時に同じユーザーが2回叩いても安全(DO の単一インスタンス性)。
const key = `grant:${ email }:${ slug }` ;
const existing = await this .state.storage. get (key);
if (existing) return new Response ( JSON . stringify ({ already: true }));
await this .state.storage. put (key, { grantedAt: Date. now () });
return new Response ( JSON . stringify ({ granted: true }));
}
return new Response ( "not found" , { status: 404 });
}
}
canonical を Agent 内に「持たせない」ことのご利益は、Agent をいつでも捨てて作り直せるという気軽さです。Agent 側のバグで journal が壊れても、最悪 30 日の TTL で勝手に消えてくれます。一方で canonical を Agent 内に持たせていると、Agent を作り直すたびにユーザー体験が壊れる恐怖と戦うことになります。これは個人開発 12 年で身に沁みた教訓です。
3階層を物理ストアにマッピングする — KV / Durable Object / R2 / D1 の判断軸
マッピング判断は、私の場合は次の3軸で機械的に決めるようにしています。
寿命:1 run / 数十日 / 永続 のどれか
並行更新:同じキーに複数 Agent が同時に書く可能性があるか
クエリ性:1キー読み出しで十分か、範囲スキャンや JOIN が要るか
これに当てはめると、Cloudflare のストア選択はかなりすっきり決まります。
寿命1run・並行更新なし・キー読み → ephemeral(in-memory Map)
寿命数十日・並行更新ほぼなし・prefix scan で足りる → journal(KV)
永続・並行更新あり・キー読み中心 → canonical(Durable Object)
永続・並行更新なし・SQL を回したい → canonical(D1)
永続・大容量・読み取り中心 → canonical(R2 にスナップショット)
私自身、最初の頃は journal も canonical も全部 KV に入れていて、prefix の付け忘れで Agent 同士が意図せず読み合うバグを 5 回くらい踏みました。3軸で機械的に判断するルールにしてからは、設計時点でほぼ事故が消えました。お勧めの判断則です。
引き継ぎ境界の設計 — どこで write back し、どこで snapshot するか
3階層を分けるだけでは不十分で、層と層の間でどう情報を渡すかも設計対象です。Antigravity の Background Agent では、私は次の3つの「境界」を意識しています。
ステップ 1. ephemeral → journal の境界(write-down)
ephemeral から「再現に時間がかかる中間決定」だけを journal に書き残します。run の終わり、あるいは Sub-agent が完了したタイミングが書き込みポイントです。すべてを書くと journal が肥大化するので、私の場合は「Agent が分岐判断を下した瞬間」だけに絞っています。
ステップ 2. journal → canonical の境界(write-back)
これが最も慎重に設計すべきポイントです。journal はあくまで Agent 内の運用ログですが、ここから canonical に書き戻すときに人間(私)の承認を挟むことを強く推奨します。Antigravity の HITL(Human-in-the-Loop)パイプラインを使うか、最低でも Slack や stand.fm の通知を1段挟みます。実際、私は過去に承認なしで journal → canonical を直結させてしまい、Agent が誤判断で premium 記事を 12 本一斉に free に降格させたことがあります。リカバリに半日かかりました。
ステップ 3. canonical → journal への snapshot(read-back)
逆方向の引き継ぎも忘れがちですが重要です。canonical の最新状態を journal に snapshot しておくと、Agent が「今の状態」を毎回 canonical に問い合わせなくて済みます。読み取りコストが減るだけでなく、canonical 側の API 制限(GitHub API の 5,000 req/h 等)にも引っかかりにくくなります。snapshot は1時間に1回程度の粒度で十分です。
この3つの境界を明示的にコードに書き分けると、Agent コード全体が驚くほど読みやすくなります。逆に境界が暗黙になっていると、コードを読んでも「今これは ephemeral なのか canonical なのか」がわからなくなり、変更時に必ず事故ります。
12年運用の壁紙アプリ群で固まった、私自身の3つの判断則
ここまで設計則を整理してきましたが、最後に、累計5,000万DLの壁紙アプリ群を 12 年運用してきた中で固まった、私自身の判断則を3つだけ共有させてください。
canonical に書く前に必ず 1 秒以上のディレイか承認を挟む。Agent の暴走で canonical を一気に書き換えられると、戻すコストは平均で半日、最悪で 2 日です。一方ディレイ 1 秒のコストは無視できます。私の場合、AdMob の eCPM サマリーを書き換える Agent では 5 秒の確認バッファを入れています。
journal は迷ったら 30 日 TTL でつけておく。後から TTL を伸ばすのは簡単ですが、TTL なしで運用したものを後から有限化するのは記憶喪失級の事故になります。Antigravity の Background Agent では journal はすべて 30 日固定で運用しています。
ephemeral を黙って捨てない。dispose() のような明示的な破棄メソッドを必ず持たせる。これだけで Cloudflare Workers の Isolate 再利用バグ・メモリリーク系トラブルが事実上ゼロになります。私自身、Browser Sub-agent で 3 日間メモリが膨らみ続けて 1 度クラッシュさせた経験があり、それ以来このルールを守っています。
3つとも、Antigravity の Agent が「将来やらかすかもしれない誤動作」を、設計段階で前もって防ぐためのものです。Agent は失敗するものとして扱ったうえで、被害範囲を 3 階層のうち最小の層に閉じ込める、というのが本稿全体の主張でもあります。
次に Background Agent を 1 本作るときに、まず「これは ephemeral / journal / canonical のどれを触る Agent か」を 1 行コメントで書いてみてください。それだけで設計レビューが半分以上終わります。実装と運用の手応えが、お互いの現場でもにじみ出てきたら嬉しく思います。お読みいただきありがとうございました。