yoi/tickets/submit-segment-protocol.md

6.0 KiB
Raw Blame History

サブミット入力: protocol Segment 化

背景

現状の Method::Run { input: String } はユーザー入力を素の文字列で運んでいる。TUI の Atom::Pastebracketed paste で受けた塊)は表示用には [Clipboard #N | X chars, Y lines] ラベルを持っているが、submit 時に InputBuffer::submit_text で flatten され、Pod や session log にはただの長い user message として伝わる。

memory / workflow 導入に伴い、submit には paste 以外にも以下を載せたくなる:

  • @<path> のファイル参照 + auto_read
  • #<slug> の Knowledge 参照(docs/plan/memory.md
  • /<slug> の Workflow 起動(docs/plan/workflow.md

これを protocol 上で String + marker パースで扱うと、Pod が正規表現と escape 規則を抱え、code block 内の #foo を literal にするか等の ambiguity と escape 漏洩が常に付きまとう。さらにリッチクライアントGUI / web / ネイティブ)はファイル添付や paste を file chip / clipboard card として描くため、protocol が String だと「chip → string marker → Pod で再 parse → 表示用に再構築」という 3 段 round-trip を全 client で再実装する形になる。

protocol を typed segment の列に上げると、Pod は parser を持たず resolve に集中でき、client は自分の表現単位のまま intent を送れる。最低限 Segment::Text(String) を fallback として置くので、CLI piping 等の dumb client は 1 segment を作るだけで従来通り動くprotocol の dictation が「Text を作れること」に閉じる)。

要件

protocol

Method::Run の payload を String から Vec<Segment> に置き換える。Segment は最低限以下の variant を持つ:

  • Text — 自由文字列。dumb client の fallback
  • Paste — TUI 等の bracketed paste 由来の塊。id と本文を持つ
  • FileRef@<path> の auto_read 対象
  • KnowledgeRef#<slug> の Knowledge 参照
  • WorkflowInvoke/<slug> の Workflow 起動

Event::UserMessage の payload も同じ Vec<Segment> に揃え、複数 client で見たり log replay する際に typed のまま再構築できる状態にする。

未知 variant に対する forward compatibility#[serde(other)] 相当の吸収)を入れる。

Pod 側 resolve

Pod は Vec<Segment> を LLM context へ flatten する単一経路を持つ:

  • Text → そのまま
  • Paste → 本文を inline 展開(現状 TUI が flatten してきたのと同じ結果)。session log / Event::UserMessage 上ではラベル化情報を保持し、表示時に [Clipboard #N | X chars] を再構築できること
  • FileRef → scope 内なら本文 read + inline、scope 外は明示エラー
  • KnowledgeRef / WorkflowInvoke → resolver が登録されていれば展開、未登録はフォールバック(後述)

resolver の trait 化と memory / workflow 用 resolver 実装は別チケット。本チケットは Text と Paste を typed のまま end-to-end で運び切る経路で完結させる。FileRef 以降は variant 定義と「resolver 未登録時のフォールバック」だけ仕込んで実装は後続に委ねる。

TUI 側

InputBuffer::Atom::Paste を submit 時に flatten せず、Segment::Paste として送出する。Event::UserMessage を typed segment で受けて Block::UserMessage を typed のまま描画paste は引き続き magenta ラベル)。

text しか作れない client が引き続き存在しても良いことを protocol 仕様に明記する(vec![Segment::Text(_)] のみで動く)。

unknown variant / 未登録 resolver の扱い

新 variant を持つ client が古い Pod に投げる、または resolver 未登録の variant を Pod が受けた場合は 2 経路に同時に流す:

  1. LLM context[unknown input: kind=foo] 相当の placeholder を Item::system_message で差し込む。LLM が「ユーザーは何かを送ろうとしたが Pod が解釈できなかった」と気づき、ユーザーに聞き返せる状態を作る。
  2. ユーザー向け通知チャネルEvent::Alert / リネーム前は Event::Notificationにも同時に送る。client 側で「このサーバはこの input type を解釈できません」とユーザーに直接出せる。

両経路に流すことで「user も LLM も気付けない silent drop」を避ける。serde 側は unknown variant を吸収する形(#[serde(other)] 相当)を初期から入れる。

Event::Alert への rename は tickets/notification-naming-cleanup.md で扱う。本チケットの実装時点での名称(Event::NotificationEvent::Alert か)はその時の repo 状態に従う。

範囲外

  • @ / # / / token の TUI parsing と補完 UItickets/submit-tui-completion.md
  • KnowledgeRef / WorkflowInvoke の resolver 実装memory / workflow チケット)
  • FileRef の引数拡張行範囲、glob 等)

完了条件

  • Method::Run / Event::UserMessageVec<Segment> で wire を通る
  • TUI から paste した内容が Segment::Paste で送出され、Pod の LLM context では本文展開、Event 経由の再描画では [Clipboard #N | ...] が復元される
  • vec![Segment::Text(_)] だけ送る client は従来通り動く
  • 未登録 variant の扱いが決定済みルール通りに動く(決定は本チケット内で合意)
  • forward compat: 未知 variant を含む payload を deserialize しても panic / parse error にならない
  • 既存ビルド・テストを壊さない

参照

  • docs/plan/memory.md §retrieval 経路 / §Knowledge の呼び出し制御
  • docs/plan/workflow.md §呼び出しと依存
  • crates/protocol/src/lib.rsMethod::Run, Event::UserMessage
  • crates/tui/src/input.rsAtom::Paste, submit_text
  • crates/tui/src/app.rssubmit_input, Block::UserMessage 描画)