diff --git a/docs/0-requirements.md b/docs/0-requirements.md index 7f0bb34..9f8ade6 100644 --- a/docs/0-requirements.md +++ b/docs/0-requirements.md @@ -6,6 +6,17 @@ アプリが持つのは**部屋の壁だけ**である。機能・ツール・認証・MCP は各 CLI がそのまま持ち、アプリはそれらを内包しない。各セッションの文脈と人格は独立したまま保たれ、発言者を識別できる共通の場で対話する。 +### 投稿モデル + +この場は AI と話すための場所ではない。人間も AI も区別なくやり取りできる場所であり、チャットよりも掲示板に近い。 + +- **参加者はひと種類である。** 人間もセッションも同じ参加者であり、違いは名前だけである。名簿は両者を同じ列に並べる。 +- **発言はひとつの行為である。** 誰が出しても同じフレームで、同じ経路を通り、全員へ配られる。発言者ごとに別の行為を用意しない。 +- **自分の発言は自分へ戻らない。** 抑止は接続の同一性で判定する。名前で判定すると、名前が衝突している間は他人の発言まで落ちる。 +- **宛先は誰でも名指せる。** 人間も宛先の選択肢に並ぶ。宛先は配送を絞らない(constraints の「単一ベンダー構成」参照)。 + +この前提は後付けではない。当初のプロトコルは人間の発言を `say`、AI の発言を `reply` として別の行為に分けており、`reply` にはそもそも他の参加者へ配る線が引かれていなかった。並列に立てた 2 セッションが互いの発言を受け取らないという実測(2026-08-22)はその帰結であり、配送行の書き忘れではなく語の形の問題だった。統一フレーム `post` はこの是正である(#39)。 + 狙いは `Liplus-Project/liplus-language` が自ら未達と記している基盤層である。判断層の Sheepdog は到達済みで、保留されているのは物理的なイベント駆動基盤(polling-on-input からの脱却)のみであり、本アプリはそこを閉じる。 設計母体は `Liplus-Project/liplus-desktop` #90。前身である liplus-desktop が掲げていた「アダプター層・タスク層・オペレーション層を UI 側に内包し、CLI にはモデル層のみを渡す」方針は撤回済みである。Li+ の L6 Adapter Layer は基盤を所有しないための層であり、内包はその反転にあたる。 @@ -88,13 +99,15 @@ liplus-chat (Tauri app) = 部屋の壁 ### 会話ループ ``` -[人間の発言] → 部屋 → MCP サーバ +[参加者の発言] → 部屋 →(発言者以外の全サイドカーへ fan-out) ↓ notifications/claude/channel [CLI 対話セッション(PTY 保持)] ↓ MCP tool 呼び出し - MCP サーバ → 部屋 → [メッセージ一覧へ追加] + MCP サーバ → 部屋 → [メッセージ一覧へ追加+他の参加者へ fan-out] ``` +人間が発言しても、セッションが発言しても、通る経路は同じ 1 本である。部屋の中に「人間の発言を処理する道」と「AI の発言を処理する道」は無い。 + push の形(参照実装 `Liplus-Project/github-webhook-mcp` `local-mcp/src/index.ts` に準拠): - method = `notifications/claude/channel` @@ -115,15 +128,21 @@ push の形(参照実装 `Liplus-Project/github-webhook-mcp` `local-mcp/src/in | 向き | type | フィールド | |---|---|---| -| 部屋 → サイドカー | `say` | `message_id` / `user` / `content` / `to`(任意) / `ts` | -| サイドカー → 部屋 | `hello` | `protocol` / `agent` | -| サイドカー → 部屋 | `reply` | `message_id` / `agent` / `content` / `to`(任意) / `ts` | +| サイドカー → 部屋 | `hello` | `protocol` / `name` | +| サイドカー → 部屋 | `post` | `message_id` / `content` / `to`(任意) / `ts` | +| 部屋 → サイドカー | `post` | `message_id` / `speaker` / `content` / `to`(任意) / `ts` | + +発言のフレームは `post` 一種類であり、向きによって名前が変わらない。誰が出した発言かはフレームの種別ではなく `speaker` の値で表す。 + +`speaker` はサイドカーが送らない。部屋が、フレームの届いた接続に紐づく名前(`hello` の `name`)から刻む。送信側が名乗れる欄を持たないため、他の参加者を騙る余地が構造上無く、名簿と表示が食い違うこともない。宛先は名指せるが自分は名乗れない、という非対称がこの性質を作っている。 + +未知の `type` は拒否せず無視する。部屋側がフレーム種別を増やしても、旧サイドカーが壊れないため。`protocol` はフレームの形が変わり、サイドカー側が気づく必要があるときに上げる。`say` / `reply` から `post` への統合で `protocol` を 2 へ上げた。サイドカーはアプリが `.mcp.json` へ書く同梱パスから起動するため、部屋とサイドカーの版が食い違う形が構造上無く、旧フレームとの互換は残していない。 -未知の `type` は拒否せず無視する。部屋側がフレーム種別を増やしても、旧サイドカーが壊れないため。`protocol` はフレームの形が変わり、サイドカー側が気づく必要があるときに上げる。 +`to` は両方向で同じ語彙であり、宛先となる参加者の表示名を入れる。任意であり、無いときは `null` ではなくキーごと省く。 -`to` は両方向で同じ語彙であり、宛先となる参加者の表示名を入れる。任意であり、無いときは `null` ではなくキーごと省く。`to` の追加で `protocol` は上げていない。サイドカーはアプリが `.mcp.json` へ書く同梱パスから起動するため、部屋とサイドカーの版が食い違う形が構造上無い。 +**自分の発言の抑止**は部屋側で行う。部屋は接続ごとに origin を採番し、fan-out のときにその発言を出した接続だけを飛ばす。origin はフレームの外側をタグとして流れ、wire には出ない。したがって送信側は自分の origin を設定することも他人の origin を騙ることもできない。名前で判定しないのは、名前が衝突している間に他の参加者の発言まで落ちるためである(#40)。サイドカー側に自己判定は置かない。 -人間の発言も、エージェントの返信も、フロントエンドへは同一の `room-message` イベントとして届く。並び順の権威を 1 箇所に保つためであり、送信時にフロント側でローカルに追記しない。 +どの参加者の発言も、フロントエンドへは同一の `room-message` イベントとして届く。並び順の権威を 1 箇所に保つためであり、送信時にフロント側でローカルに追記しない。イベントは `own`(自分の発言かどうか)を伴う。表示上の区別を残す場合の軸はこれであり、人間か AI かではない。 ### サイドカーの起動形 @@ -141,9 +160,12 @@ push の形(参照実装 `Liplus-Project/github-webhook-mcp` `local-mcp/src/in 判定材料(`meta.to`)と判定規律(`instructions`)は同時に変える。片方だけ動かすと、宛先を知らないまま宛先を判断させる状態になる。 - サイドカーは自分の部屋での名前(`LIPLUS_AGENT_NAME`)を `instructions` へ埋め込む。埋めなければエージェントは `meta.to` を自分と照合できない。 +- `instructions` は「人間と AI を区別しない」ことを明示する。区別しない前提はフレームだけでなく作法にも要る。フレームから区別を消しても、作法が「人間に答えよ」と言っていれば、エージェントはもう無い区別を読むことになる。 - `meta.to` が自分の名前なら自分宛、他の参加者の名前なら自分宛ではない、無ければ部屋全体宛。全体宛に誰が答えるかはエージェントの判断に委ねる。 - 宛先による配送の絞り込みはアプリ側で行わない。部屋は全員へ配り、答えるかどうかをエージェントが決める。絞れば部屋が「誰に届いたか」を持つことになり、アプリが会話の中身へ関与し始める。 - 宛先は任意である。必須にすると人間の入力費用が上がる。 +- 宛先の相手は人間でもセッションでもよく、指定の仕方は変わらない。画面の宛先の選択肢は「自分以外の参加者」であり、人間も同じ列に並ぶ。 +- 届いた発言はすべて他の参加者のものである。自分の発言は戻らないため、エージェントは自分を認識する必要がない。 ### MCP サーバの実装方式 @@ -171,8 +193,9 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre - 部屋ソケット(Rust、`127.0.0.1` 任意ポート、Bearer トークン必須) - サーバの `.mcp.json` 登録(既存内容はマージして保持)と、成立条件を満たす CLI 起動フラグの適用 - 部屋の作法(`instructions`)の初版 -- 発言の宛先(部屋 → `say.to` → channel の `meta.to`、返信は `say_to_room` の `to`)と、それを判定材料として名指しする `instructions` -- チャットルーム UI(メッセージ一覧、発言者表示、入力欄、参加者表示) +- 発言の宛先(部屋 → `post.to` → channel の `meta.to`、送信は `say_to_room` の `to`)と、それを判定材料として名指しする `instructions` +- 参加者モデル(統一 `post` フレーム、発言者以外の全参加者への配送、接続同一性による自分の発言の抑止、人間を含む名簿) +- チャットルーム UI(メッセージ一覧、発言者表示、入力欄、参加者表示、自分以外の参加者から選ぶ宛先) - 診断面(部屋ソケットの待受状態、セッションの生死と終了コード、CLI の端末表示と入力) - Windows CI(`npm ci` と Rust コンパイル確認)、Release 公開時の CD @@ -185,7 +208,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre ### 未実装 -- 複数の AI セッションを同一の部屋へ参加させる運用(同時発話の抑制を含む) +- 複数の AI セッションを同一の部屋へ参加させる運用(同時発話の抑制を含む)。参加者間で発言が届く経路は実装済みだが、実機での往復はまだ確認していない。2026-08-22 の実測(2 セッションが互いの発言を受け取らない)は #39 の修正前のものであり、修正後の再計測は済んでいない。 - 会話ログの永続化と観測 UI - plugin としての allowlist 掲載(配布の第二段階) @@ -252,7 +275,7 @@ CI が実行するもの: 1. **部屋ソケットが待ち受けているか。** タイトルバーの「診断」を開き、部屋ソケットの行を見る。`127.0.0.1: で待受中` でなければ以降はすべて不成立。 2. **セッションが生きているか。** 同じ診断のセッション行を見る。`終了(終了コード N)` なら CLI が落ちている。直下の端末に理由が残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。 -3. **サイドカーが接続したか。** 参加者表示にセッション名が出る。出ない場合は端末に `room socket: connected as ""` があるかを見る。 +3. **サイドカーが接続したか。** 参加者表示に**セッション名**が出る。自分(人間)は発言前から名簿に載っているため、参加者表示が空でないことは接続の根拠にならない。セッション名が出ない場合は端末に `room socket: connected as ""` があるかを見る。 4. **サイドカー単体が壊れていないか。** `npm run sidecar:test` を実行する。偽の部屋ソケットを立てて両面を駆動するため、通ればサイドカーではなくアプリ側の問題に絞れる。 5. **`.mcp.json` の登録が効いているか。** セッションの作業ディレクトリの `.mcp.json` に `liplus-chat-room` が入っているかを見る。`--mcp-config` によるファイル渡しでは channel 側が名前を解決できないため、正式登録以外の経路は成立しない。 6. **起動フラグが単独指定になっているか。** `--dangerously-load-development-channels server:liplus-chat-room` 以外に `--channels` が付いていると、同一サーバが二重登録され全体が不通になる。タブ設定に `--channels` / `--print` / `--input-format` / `--output-format` がある場合、セッション起動はエラーで止まる(黙って外すと動いたように見えるため)。 diff --git a/index.html b/index.html index a7c563c..257daf5 100644 --- a/index.html +++ b/index.html @@ -60,9 +60,10 @@