Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 は基盤を所有しないための層であり、内包はその反転にあたる。
Expand Down Expand Up @@ -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`
Expand All @@ -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 かではない

### サイドカーの起動形

Expand All @@ -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 サーバの実装方式

Expand Down Expand Up @@ -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

Expand All @@ -185,7 +208,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre

### 未実装

- 複数の AI セッションを同一の部屋へ参加させる運用(同時発話の抑制を含む)
- 複数の AI セッションを同一の部屋へ参加させる運用(同時発話の抑制を含む)。参加者間で発言が届く経路は実装済みだが、実機での往復はまだ確認していない。2026-08-22 の実測(2 セッションが互いの発言を受け取らない)は #39 の修正前のものであり、修正後の再計測は済んでいない。
- 会話ログの永続化と観測 UI
- plugin としての allowlist 掲載(配布の第二段階)

Expand Down Expand Up @@ -252,7 +275,7 @@ CI が実行するもの:

1. **部屋ソケットが待ち受けているか。** タイトルバーの「診断」を開き、部屋ソケットの行を見る。`127.0.0.1:<port> で待受中` でなければ以降はすべて不成立。
2. **セッションが生きているか。** 同じ診断のセッション行を見る。`終了(終了コード N)` なら CLI が落ちている。直下の端末に理由が残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。
3. **サイドカーが接続したか。** 参加者表示にセッション名が出る。出ない場合は端末に `room socket: connected as "<name>"` があるかを見る。
3. **サイドカーが接続したか。** 参加者表示に**セッション名**が出る。自分(人間)は発言前から名簿に載っているため、参加者表示が空でないことは接続の根拠にならない。セッション名が出ない場合は端末に `room socket: connected as "<name>"` があるかを見る。
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` がある場合、セッション起動はエラーで止まる(黙って外すと動いたように見えるため)。
Expand Down
7 changes: 4 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@

<footer id="composer">
<!-- The addressee is optional. Leaving it on 全体 is a room-wide
utterance; who answers one of those is the agents' judgment, not
the room's. The room delivers every utterance to everyone either
way. -->
utterance; who answers one of those is the participants'
judgment, not the room's. The room delivers every utterance to
everyone either way, and anyone in the roster can be named —
people and sessions alike. -->
<label class="to">
宛先
<select id="to-select">
Expand Down
Loading