hachiflow.com
アーキテクチャ

バスこそが基盤

毎日使っている 2 つのプロダクトへの不満を、はっきり言葉にするのに 1 年かかりました。一方はシングルプレイヤーを完成させ、そこで止まりました。もう一方は人が集まる場を正しくとらえ、そこにエージェントを溶接しました。Waggle はその不満から生まれたアーキテクチャです。以下はその証跡です。ルール ID、イベント型、トレース、そして 2 日目に見つけたバグ。

2 つのプロダクト、足りない 1 つのピース

Grokbot はシングルプレイヤー体験を制覇し、その地位は誰にも奪われていません。1 人、1 つのボット、1 台のコンピューター。ループはあまりにタイトで、ソフトウェアの存在を意識しなくなるほどです。クローンたちも同じ結論に至っています。私は Apache-2.0 の Grok Bot クローンである Rakazo のコミット 616d2353 を 1 日かけて読みましたが、これは良い仕事です。保留中のツール呼び出しを永続化し、モデルに再度導出させるのではなく、承認済みの引数をそのまま再生する承認の仕組みは、盗む価値があります。

次に、コードを変えずに振る舞いを変えられる部分を探しました。ありませんでした。イングレスは宛先に溶接されています。Webhook の URL は POST /api/v1/bots/:botId/webhook で、呼び出す側がボットを名指しするので、ルーティングする余地は残っていません。ルールという成果物がないので、差分を取るものもありません。トレース ID も永続化されません。AdapterContext.traceId は実行 ID を運ぶログのフィールドにすぎず、どの実行も親を記録しないので、ボットからボットへ 6 ホップ続くチェーンはどんなクエリでもたどり直せません。そこにあるものは何一つ最適化できず、どのエージェントも去ることができません。

Buzz はこの不満のもう半分で、私はそのホスティングを売っているので、正直に辛口になる資格があります。Buzz には Grokbot が表現できないものがあります。部屋に複数の人間がいて、エージェントが従量課金のアドオンではなくメンバーとして存在することです。メンバーとは鍵ペアです。エージェントとは、その認証イベントが所有者の署名を運ぶ鍵ペアであり、ボットのバッジとアテステーションはアプリの飾りではなくリレー上の事実です(仕組みはこちら)。それ以外の部分はすべて溶接されています。アイデンティティはデスクトップ上の Keys::generate() で鋳造され、秘密鍵はデプロイのペイロードに乗って実行器まで運ばれ、本体はどこで起動されたかによって動き続けます。ノートパソコンを閉じれば、あなたのチームメイトは消えます。

バスこそが基盤

Waggle は、そのすべてを溶接することを拒んだときに残るものです。交換可能な 6 つの部品。イングレスが信頼とアイデンティティを刻印します。追記専用ログがすべてのイベントを保持します。ルールファイルが誰を起こすかを決めます。実行器は交換可能な宛先です。認証情報はリースとプロキシを備えたエスクローに置かれます。1 つのトレース ID がすべてをつなぎます。

その何ひとつとして抽象的ではありません。イベントは ev.<tenant>.<source>.<type> に着地し、ほとんどのシステムが混同している 2 つのフィールドを運びます。principal、誰がそれを置いたか。そして trust、その主張がそもそも検証されたかどうかです。有効な署名を持つ Stripe の投稿は、パートナー信頼レベルの partner:stripe です。同じ投稿でも署名が不正なら 401 になり、そもそも追記すらされません。ID はテナントを含む冪等性キーから導出されるので、同じイベントが 2 回配送されても同じ ID になり、ログは 2 つ目のコピーを捨てます。

ルーターは TOML ファイルを読みます。プロンプトでも、データベースの行でもありません。ルール ID が並ぶファイルで、ホットリロードされ、書き込まれる前に実運用中のレジストリと照らして検証され、1 つのテナント内で 2 つのルールが ID を共有していれば丸ごと拒否されます。このファイルこそが成果物です。プルリクエストで差分を取るもの、エージェントが変更を提案できるもの、そしてトレースが何かが起きた理由を語るときに名指すものです。宛先もまた行であり、配送先を持つので、エージェントの実行場所を変えることは 1 行を編集することに等しくなります。

Buzz、Slack あらゆるサーフェス Webhook 署名ありもなしも cron 時刻で イングレス テナントとプリンシパル 信頼はここで刻印 ルールファイル r-cos2-addressed 誰が、なぜ起きるか 実行器 1 回の覚醒に 1 ターン エスクロー リースとプロキシ サーフェス リレーとメンバー一覧 プレースホルダー ev.<tenant>.<source>.<type> 追記専用 hachi trace キー入力からモデルの呼び出しまで ID 1 つ
交換可能な 6 つの部品。サーフェスはイングレスのソースと配送先の 2 つにすぎないので、Buzz も Slack も書き直しではなく行の追加で済みます。エスクローは実行器にプレースホルダーを渡し、実値はプロキシで差し替えます。だからモデルが鍵を読むことはありません。すべては 1 つのログに、同じトレース ID の下で着地します。

エージェントは話さず、発する

私が信頼するまでに一番時間がかかったルールが、今では一番気に入っているルールです。エージェントは会話をするのではなく、イベントを発し、エージェント宛てのイベントがそのエージェントを起こします。チャット用のメッセージバスと業務用の別のバスがあるわけではありません。ログは 1 つだけです。返信とは addressed_to が設定されたイベントであり、人間が見ていようといまいと、トレースはそれを記録します。

実際のターンを動かし始めて 2 日目、それが偽りだと気づきました。9 月 8 日 05:07:59Z、私は実際の Claude ランタイム上のチーフ・オブ・スタッフである agent:cos2 に、そのマーケティング担当エージェントについて尋ねました。cos2 は正しいことをしました。agent:growth 宛てに ask を発したのです。イベント 3YRSRD08JX549QHDHCXPZXRK7P、トレース 7MDV4GJYX2TWTMQH5SP23PYSJ8 の下で。それに一致するものは何もありませんでした。生成されたルールは覚醒用の行、コンソールメッセージ、エスクローの通知には一致していましたが、件名が別のエージェントであるパートナーイベントにはどれも一致しなかったのです。ask はデフォルトの宛先に落ち、growth は一度も起きず、cos2 の実行は 3 秒後に終了し、チャットには何も表示されませんでした。

この修正こそが、この形が正しいのだと私を納得させたものです。なぜならそれは新機能ではなかったからです。すべてのエージェントに生成される標準ルールが 1 つ増えただけでした。r-<name>-addressed という、subject = "agent:<name>" と trust = "partner" に一致するルールを、エージェント自身の覚醒用の行の後ろに置くことで特定のルールが常に優先され、既存のすべてのエージェントに一度だけそれを追加する起動時マイグレーションと、自分宛てに発することを拒むイングレス側のチェックを添えました。エージェント同士の会話は、バスの上では生成された TOML の 5 行です。それがなければ、公開日だけが決まったロードマップの項目です。

モデルは鍵を読まない

認証情報こそが、これが単なるアーキテクチャの好みの問題ではなくなる場所です。サブスクリプションのトークンはエスクロー内の権限であり、サービスは claude_code、フィールドは oauth_token、ホストは api.anthropic.com です。覚醒時にエスクローがリースを発行し、コンテナにはプレースホルダー __claude_code_oauth_token__ が渡されます。これはまさに Claude Code がトークンを読み取る変数名です。コンテナの唯一の出口は、そのホストに限ってリクエストヘッダーの実値を差し替えるプロキシです。モデルが見るのはプレースホルダーです。ディスクも同様です。通過した実行のジャーナルには AGENTMAIL_API_KEY=__agentmail_api_key__ と書かれています。プロセスに渡されたのがそれだからです。

すべてのステップはイベントであり、どれも値を運びません。credential.requested、credential.granted、覚醒時の credential.leased、プロキシが見たリクエストごとの credential.used、取り戻すときの credential.revoked。監査証跡は誰かが書こうと思い出したログではなく、他のすべてと同じログです。功績は正しい場所に。ランナーとブローカーの形は Jake Gaylor によるもので、Fountain から来ています。彼はチームメイトごとにパークしたり起きたりするサンドボックスと、エージェントが決して読むことのないテナントの推論用認証情報を、私がこの仕組みを何一つ持たないはるか前から実現していました。アンチパターンははっきり名指ししておきましょう。鍵を環境変数に置けば、モデルが実行するあらゆるシェルコマンドがそれを読めてしまい、部分文字列を編集で隠す程度ではそれを防げません。base64 が存在するからです。

人間は 1 人、そしてトレースがチャンネルになる

では、チャットウィンドウはどこへ行くのでしょうか。人間が 1 人なら、どこにも行きません。Waggle がインターフェースであり、トレースがチャンネルです。入ってきたメッセージ、一致したルール、覚醒、モデルのツール呼び出し、その発したイベント、終了コード、そのすべてが印字できる 1 つの ID の下にあります。hachi trace は、イングレスのホップ、発火したルール、確認応答した宛先、そしてその原因となったホップの下にあるすべての子を表示します。それを会話として描くのはビューであって、2 つ目のプロダクトではありません。

2 人目の人間が加わると、アーキテクチャを変えずに要件だけが変わります。2 人が必要としているのは、メンバー一覧、プレゼンス、履歴を備えた共有の場所であり、それはまさにチャットプロダクトが得意とすることであり、バスがなろうとすべきではないものでもあります。つまりサーフェスとは、イングレスのソースと配送先という 2 つのありふれたものにすぎません。

Buzz の場合、それは紐づいたエージェントごとの 1 本のリレー接続であり、受理されたすべてのイベントは、入ってくる際に署名が再検証される buzz.message のエンベロープとして追記され、ふつうのルールがそれをエージェントへ運び、エージェントの answer を送り返し、配送先がエージェントとして応答に署名します。このデモの見積もりは、6 つの項目にわたって約 4 日でした。そのどれも新しい概念ではありません。Buzz はすでにプロバイダーのバイナリを呼び出す方法を知っています。Slack も同じ 2 つのものに、違うネクタイを締めているだけです。Buzz はサーフェスであるべきです。Slack はサーフェスです。

エージェントはあなたのもの、職場は彼らのもの

実行、推論、認証情報、アイデンティティが別々のものになると、所有権は哲学的な問いであることをやめ、4 つのフィールドになります。所有権はエージェントの側にあります。推論、実行、データはバインディングの側にあります。バインディングこそが職場が保持するものです。どのサブスクリプションがそのターンの代金を払うか、どの実行器がそれを実行するか、そして何を保持してよいかです。エージェントの ID はそれ自身の公開鍵であり、作成時に鋳造され、秘密鍵の半分はエスクロー内の権限としてデーモンの内部でのみ明かされ、実行器に貸し出されることは決してありません。

体験はプライバシーの境界線で分かれます。職人技のノートは所有者に属し、共に移動します。テナントのノートは職場に属し、そこから出ることは決してありません。パスポートはアイデンティティ、ソウル、職人技、そして値の入っていない所有者の権限への参照を運びますが、企業の記憶を運ぶことは決してありません。それらの権限を別のインスタンスへ運ぶための鍵束は、アップストリームで block/buzz PR #6011 として草案が進んでいます。この条件は約束ではなく検証可能なものです。mini 上で、私はあるバインディングをサブスクリプションの権限を 1 つも持たない所有者向けに inference: owner に設定しました。その次の覚醒は credential.refused を生成し、その所有者の claude_code 権限は、そのテナントのそのエージェントをどれもカバーしていない、と言葉で告げました。条件はドキュメントではありません。リースの発行者が読むものです。

数字、小さくて本物

これはどれもクラスター上では動いていません。Mac mini 1 台と docker compose です。チーフ・オブ・スタッフを作るところから、マーケティングがプレースホルダーとしてしか目にしないリースを受け取って目覚めるところまで、ストーリーボード全体は最初の作成から PASS まで 15 秒、コンテナのターンは 4 回です。環境ファイル内の 8 つの秘密情報はそれぞれ、ログがマシンの外に出る前に、エンドツーエンドのログとビルドログの両方に対して grep されました。一致はそれぞれゼロ件です。実行が貼り付ける偽の鍵は、どのログ、設定、送信トレイ、トレースにも見つかりませんでした。

計画では、5 つのフェーズにわたってエンジニア 1 人で 39〜54 営業日、さらに所有権のマイルストーンに 14〜24 営業日を見積もっていました。エージェントの群れが 5 つのフェーズを構築し、9 月 7 日に mini 上で成功しました。所有権のマイグレーションは 9 月 8 日の起動時に一度だけ実行され、既存の 13 のエージェントをそれぞれ独自のアイデンティティを持つ所有されたオブジェクトに変え、生成済みの 33 のルールには手を触れませんでした。両方合わせて 2 日です。検証対象のカバレッジは 88.7 パーセントで、ライブのビルダースイートは 15 件中 15 件が通過しています。以下は欠けている部分です。それを書かないフィールドノートは広告になってしまうからです。プレースホルダーを差し替えたプロキシ経由の実際の外向き呼び出しは、mini ではなくローカルのアップストリームに対するテストでカバーされています。また、上で説明した Buzz とのバインディングは、出荷済みの機能ではなく、リレーのソースコードに対する設計上の読み取りにすぎません。

基盤こそがプロダクトである

ここからはニスを剥がした売り込みです。基盤こそがプロダクトです。サーフェスはコモディティです。チャットアプリとはメンバー一覧とリレーとテキストボックスであり、来年にはもっと良いものが出てきます。実行器もコモディティです。今日はコンテナ、明日はレンタルの計算資源、その次はマイクロ VM です。モデルもコモディティであり、意図的に安くなり続けています。コモディティではないもの、それは何が起きたか、なぜ起きたか、そして何に触れることを許されたかの記録です。その記録こそが、誰もがそれに対して最適化できる唯一の成果物です。堀はトレースです。

だから売り込みの代わりにテストをしてみてください。あなたのエージェントが最後に間違ったことをしたときのことを思い出し、二度と起きないようにするにはどの成果物を編集すればよいか、自分に問うてみてください。答えが「もっと長いプロンプト」なら、あなたが持っているのはボットです。答えが「差分の取れるルール ID」と「印字できるトレース ID」なら、あなたが持っているのはバスです。バスを作りましょう。

← フィールドノート一覧