diff --git a/Cargo.lock b/Cargo.lock index aed8cfd3..98b337e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,6 +1589,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + [[package]] name = "once_cell" version = "1.21.4" @@ -1925,6 +1931,7 @@ dependencies = [ "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termion", "ratatui-termwiz", "ratatui-widgets", ] @@ -1971,6 +1978,17 @@ dependencies = [ "ratatui-widgets", ] +[[package]] +name = "ratatui-termion" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cade85a8591fbc911e147951422f0d6fd40f4948b271b6216c7dc01838996f8" +dependencies = [ + "instability", + "ratatui-core", + "termion", +] + [[package]] name = "ratatui-termwiz" version = "0.1.0" @@ -2623,6 +2641,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "termion" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" +dependencies = [ + "libc", + "numtoa", +] + [[package]] name = "termios" version = "0.3.3" @@ -3013,6 +3041,7 @@ dependencies = [ "ratatui", "serde_json", "tokio", + "unicode-width", ] [[package]] @@ -3052,9 +3081,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" diff --git a/TODO.md b/TODO.md index 8c64ae24..af5d42dc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,10 @@ - [ ] テスト設計 → [tickets/test-design.md](tickets/test-design.md) - [ ] ツール設計 - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) + - [ ] ツール実行結果のサイズ上限 → [tickets/tool-output-limit.md](tickets/tool-output-limit.md) - [ ] 複数 Pod 間の Scope 排他制御 → [tickets/scope-exclusion.md](tickets/scope-exclusion.md) - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) +- [ ] システムプロンプトのテンプレート化 → [tickets/system-prompt-template.md](tickets/system-prompt-template.md) +- [ ] AGENTS.md の取り込み → [tickets/agents-md-ingestion.md](tickets/agents-md-ingestion.md) diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index b733fff9..d1f74a0f 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -6,7 +6,8 @@ license.workspace = true [dependencies] protocol = { path = "../protocol" } -ratatui = "0.30.0" +ratatui = { version = "0.30.0", features = ["scrolling-regions"] } crossterm = "0.28" tokio = { version = "1.49", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] } serde_json = "1.0" +unicode-width = "0.2.2" diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index c4444a2f..13d06ea9 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -3,6 +3,7 @@ use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Padding, Paragraph, Wrap}; +use unicode_width::UnicodeWidthStr; use crate::app::{App, MessageKind, OutputItem, fmt_tokens}; @@ -153,7 +154,7 @@ fn draw_input(frame: &mut Frame, app: &App, area: Rect) { ]); frame.render_widget(Paragraph::new(line), area); - let cursor_x = area.x + 2 + app.input[..app.cursor].chars().count() as u16; + let cursor_x = area.x + 2 + UnicodeWidthStr::width(&app.input[..app.cursor]) as u16; let cursor_y = area.y; frame.set_cursor_position(Position::new(cursor_x, cursor_y)); } diff --git a/tickets/agents-md-ingestion.md b/tickets/agents-md-ingestion.md new file mode 100644 index 00000000..06739c43 --- /dev/null +++ b/tickets/agents-md-ingestion.md @@ -0,0 +1,42 @@ +# AGENTS.md の取り込み + +## 背景 + +[agents.md](https://agents.md) は AI コーディングエージェント向けにプロジェクト固有の指示を置く標準ファイルとして普及しつつある(主要エージェント群が対応し、OpenAI の本体リポジトリだけでも 88 個の AGENTS.md が置かれている)。Insomnia の Pod も、作業ディレクトリに配置された `AGENTS.md` を自動でシステムプロンプトの文脈として取り込めるようにしたい。 + +agents.md 仕様では「編集対象ファイルから見て最近接の1つ」を読むモデルだが、Insomnia の Pod は cwd が1点に固定されるため、**cwd 直下の `AGENTS.md` のみ**を対象とする。サブプロジェクト向けにカスタム文脈を与えたい場合はそのディレクトリを cwd にして Pod を立てる運用を想定する。 + +## 依存 + +- `system-prompt-template.md`: 本チケットは読み取り結果をテンプレート変数として露出する前提に立つ。テンプレート機構が無いと組み込み先が無いため、先に着手する。 + +## 要件 + +### 探索 + +- Pod の cwd 直下に `AGENTS.md` があれば読む。親ディレクトリへの遡及は行わない。 +- 子ディレクトリのネスト AGENTS.md も扱わない(cwd 直下の1ファイルのみ)。 +- ファイルが存在しない場合は欠損ではなく「空」として扱い、エラーにしない。 + +### テンプレートへの露出 + +- `system-prompt-template.md` で定義したテンプレート変数の1つとして AGENTS.md の本文を提供する。 +- 評価タイミングはテンプレート本体と同じく first turn 開始時の1回のみ。compact 後も再評価しない。 +- ファイルが存在しないときに変数が参照された場合の値(空文字 / 未定義扱い)をテンプレート側の未定義変数ポリシーと整合させる。 + +### 例外処理 + +- ファイルサイズの上限と、上限超過時の扱い(切り詰め / エラー / 警告)を決める。 +- 非 UTF-8 等の読み取り失敗時の扱いを決める。first turn 開始の失敗として扱うか、空として続行するかは `system-prompt-template.md` のエラー処理方針に揃える。 + +## 完了条件 + +- Pod の cwd 直下に `AGENTS.md` を置いて Pod を起動すると、その内容が first turn のシステムプロンプトに反映される。 +- `AGENTS.md` を置かない場合でも Pod が正常に起動し、システムプロンプトが壊れない。 +- compact を跨いで AGENTS.md の内容が再読込されないことを担保する。 + +## 範囲外 + +- 親ディレクトリ方向への遡及、ネスト AGENTS.md のマージ。 +- ユーザ単位の共通設定ファイル(将来 Insomnia 独自の user config として別チケット化)。 +- AGENTS.md 以外のフォーマット(CLAUDE.md 等)への自動対応。必要なら各自シンボリックリンクで解決する前提。 diff --git a/tickets/system-prompt-template.md b/tickets/system-prompt-template.md new file mode 100644 index 00000000..cb9f4e0f --- /dev/null +++ b/tickets/system-prompt-template.md @@ -0,0 +1,45 @@ +# システムプロンプトのテンプレート化 + +## 背景 + +現状、`WorkerManifest.system_prompt` は単なる `Option` で、マニフェスト記述時点の固定テキストしか持てない。実行時に決まる情報(日付、cwd、scope、利用可能なツール、外部ファイルの内容など)をシステムプロンプトに埋め込む手段が無く、Pod ごとに文脈を調整したいケースに対応できない。 + +AGENTS.md の取り込みをはじめ、今後システムプロンプトへ差し込みたい情報は増えていく見込みで、その受け皿としてテンプレート機構を先に固める。 + +## 要件 + +### 評価モデル + +- ツールファクトリと同じく**遅延評価**。マニフェストの値はテンプレート定義として保持し、文字列への materialize は Pod がワーカーを起動して**最初のターンが開始されるタイミングで1回だけ**行う。 +- 一度 materialize したシステムプロンプトは以降のターンを通じて同一値を使う。**compact 後も再評価しない**(compact の前後でシステムプロンプトが変化しないことを保証する)。 + +### テンプレート構文と変数 + +- プレースホルダ構文を1つ決める(既存エンジン採用 or 最小独自記法)。選定理由を背景に書き残す。 +- 組み込み変数のセットを定義する。初期セットの候補: + - 日付 / 時刻 + - Pod の cwd + - scope 情報(読み取り可能パス等の要約) + - 利用可能なツール一覧 + - AGENTS.md 等の外部ファイル(別チケットで値を供給する前提で、キー空間だけ確保する) +- 未定義変数を参照したときの挙動(エラー / 空文字 / 警告ログ)と、リテラルとして `{{` を出したい場合のエスケープ方法を定める。 + +### マニフェスト上の記法 + +- 既存フィールド `system_prompt` をそのままテンプレート文字列として解釈するか、テンプレート用フィールドを新設するかを決める。 +- マニフェストのパース段階ではテンプレートの構文検査のみ行い、変数解決は行わない(遅延評価の原則を崩さない)。 + +### エラー処理 + +- テンプレート展開時のエラー(未定義変数・構文エラー・外部入力の失敗)が first turn 開始時に起きた場合の扱いを決める。ワーカー起動失敗として扱うか、フォールバックで進めるか。 + +## 完了条件 + +- マニフェストに書いたシステムプロンプトがテンプレートとして解釈され、少なくとも組み込み変数の数種類(例: 日付、cwd)が first turn 開始時に展開されて LLM への system メッセージに反映される。 +- compact を挟んでもシステムプロンプトが再評価されないことをテストで担保する。 +- 外部ファイル系の変数(AGENTS.md など)は別チケットで供給するため、本チケットでは「変数として受け取れる器」までを用意する。 + +## 範囲外 + +- AGENTS.md の読み取り自体は別チケット(`agents-md-ingestion.md`)で扱う。 +- ユーザ単位の共通設定ファイル(Insomnia 独自の user config)は本チケットのスコープ外。 diff --git a/tickets/tool-output-limit.md b/tickets/tool-output-limit.md new file mode 100644 index 00000000..ffd93687 --- /dev/null +++ b/tickets/tool-output-limit.md @@ -0,0 +1,63 @@ +# ツール実行結果のサイズ上限 + +## 背景 + +Pod のセッションで、Glob が `pattern:"*"` でプロジェクト全体を走査し、 +約 125KB / 推定 70k トークン超の tool_result を返した結果、次ターンのリクエストが +組織レートリミット(30k input tokens/分)を単発で超過して永久に 429 で詰む事故が発生した。 + +一度肥大化した履歴は prune/compact が走る前に再送され続け、待っても抜けられない。 +根本原因はツールが「呼ばれた通りの結果を素直に全部返す」こと。ツール自身の件数上限 +(例: `Glob` の 1000 件)はバイト/トークン単位の上限ではないため機能しない。 + +ツール実行結果のサイズを LLM に投げる前に強制的にキャップし、LLM に +「検索範囲を絞れ」と促す必要がある。 + +## 要件 + +- **単一チョークポイントで全ツールに効く**: 個別ツールの実装を信用しない。 + Tool 実行境界(llm-worker の Tool runner)で `ToolOutput.content` をトークン計測し、 + 上限を超えていたら切り詰めてから履歴に積む。 +- **マニフェストで設定可能**: デフォルトは 5000 トークン(30k/分レートリミットに + 対して余裕を持った値)。プロジェクトごと・ツールごとに上書き可能。 +- **切り詰め後は LLM が検知できる**: `summary` 又は `content` 末尾に + `truncated: N tokens dropped, refine your query` 形式の追記を入れ、 + LLM が自発的に絞り込みを試みるヒントにする。`ToolError` にはしない + (エラーにすると LLM がリトライループに入りやすい)。 +- **計測は既存の `token-counter` クレートを使う**。文字数やバイト数で近似しない。 + +## マニフェスト + +```toml +[worker.tool_output] +# 全ツール共通の既定上限。省略時 5000。 +default_max_tokens = 5000 + +# ツールごとの上書き。ツール名は登録名("Glob", "Read", ...)。 +[worker.tool_output.per_tool] +Read = 8000 # Read は大きいファイルを意図的に返すので少し緩める +Grep = 3000 +``` + +- `[worker.tool_output]` セクション自体は省略可能。省略時はデフォルト 5000 が全ツールに適用。 +- `per_tool` も省略可能。 +- 未知のツール名がマップに含まれていても manifest エラーにはしない(ログ警告のみ)。 + +## 実装方針(実装順序) + +1. `token-counter` を llm-worker の依存に追加(まだなら)し、Tool 実行境界の + `ToolOutput` を計測する薄いラッパーを導入 +2. 超過時の切り詰めロジック(content を二分探索などでトークン数ぎりぎりまで詰め、 + 末尾に注記を追記) +3. `manifest::WorkerManifest` に `tool_output: Option` を追加、 + llm-worker の `Worker` 生成時に渡す +4. 各ツール単体には本チケットでは手を入れない。上限を踏んだツールに対して + 後続の改善(Glob が `git_ignore` を尊重する等)は別チケットで扱う + +## 非ゴール + +- **ツール固有の賢い縮退**(Glob が件数で、Read が行範囲で、など)は扱わない。 + まず一律上限で事故を止め、各ツールの自主制限は必要に応じて別チケットで追加する。 +- **prompt caching の導入**や compaction 側の改善は扱わない。 + 本チケットは「1 回のツール結果が履歴に載る前にキャップする」ことだけに集中する。 +- **入力側(ツール引数)のサイズ制限**は扱わない。