cratesの整理

This commit is contained in:
2026-04-11 02:48:50 +09:00
parent cff082ff3a
commit 4c3f81b4fa
34 changed files with 2524 additions and 669 deletions
-17
View File
@@ -1,17 +0,0 @@
# insomniaが求めるllmクライアントライブラリ
- 前提:
a. userメッセージを追加しなくてもagentの途中ママ投げれば、AIはそれを自身の生成途中と認識して普通に継続する。
b. KVキャッシュは速度・効率の面で有利で、基本的にコンテキストを重ねた後での事後的なコンテキストの改変はKVキャッシュヒット率を大幅に下げる
c. ツール・フックの基本的なスキーマ自動化を提供する
以上を前提とし、私が求める性能:
- メッセージの送信と生成のResume、一時停止/再開が必要。
- 暗黙的にキャッシュを保証し、キャッシュを破壊しうる操作を明示的にブロックせずとも、いつの間にかキャッシュ破壊してた状態にはしたくない。
## 上層の要件
おそらく最下層の抽象化レイヤーではなく、その上でやるべき事
- Hooksの実装
- 送信コンテンツの操作やエージェントの生成にしたがって発生するイベントを適切に処理できる仕組み
-89
View File
@@ -1,89 +0,0 @@
# 永続化設計
## 概要
`llm-worker-persistence` クレートは、`llm-worker``Worker` セッション状態を
JSONL append-only ログとして永続化する。ログを読み込んで集約することで Worker 状態を復元する。
## 設計方針
- **JSONL append-only ログ**: 1セッション = 1つの `.jsonl` ファイル。書き込みは末尾追記のみ。
- **Pause/正常終了で構造に差異なし**: Worker の状態は Pause 時も正常終了時も同じ形
(`history: Vec<Item>` + `turn_count` + `request_config`)。
`resume()` は「ユーザー入力を追加せず `run_turn_loop()` に再入する」だけなので、
復元に必要なのは history の中身であり、前回の終了理由ではない。
`RunOutcome``Finished`/`Paused` 区分は監査用メタデータであり、状態復元の分岐には使わない。
- **クレート分離**: `llm-worker` は永続化を知らない。`Session` ラッパーが外から Worker を包む。
## 命名規約
| 名前 | 用途 |
|---|---|
| **SessionLog / LogEntry** | 状態復元用の構造化された記録(永続化の本体) |
| **EventTrace / TraceEntry** | デバッグ用の生ストリームイベント全録(オプション、デフォルト OFF) |
## クレート構成
```
llm-worker-persistence → llm-worker → llm-worker-macros
```
`llm-worker-persistence``llm-worker` に依存するが、逆方向の依存はない。
## ファイル配置
```
{root}/{session_id}.jsonl -- セッションログ
{root}/{session_id}.trace.jsonl -- イベントトレース(デバッグ時のみ)
```
`SessionId` は UUID v7`uuid` クレート)。タイムスタンプ埋め込みで辞書順 = 時系列順。
## LogEntry
各エントリは Worker の特定の状態変更に対応する:
| エントリ | Worker 上の対応箇所 | collect_state での効果 |
|---|---|---|
| `SessionStart` | セッション開始 / fork | system_prompt, config, history を初期化 |
| `UserInput` | `worker.rs:229` | history に追加 |
| `AssistantItems` | `worker.rs:1040-1041` | history に追加 |
| `ToolResults` | `worker.rs:897-900, 1072-1076` | history に追加 |
| `HookInjectedItems` | `worker.rs:1055` | history に追加 |
| `TurnEnd` | `worker.rs:1033` | turn_count を更新 |
| `CacheLocked` | `Worker::lock()` | locked_prefix_len を設定 |
| `CacheUnlocked` | `Worker::unlock()` | locked_prefix_len を 0 に |
| `RunOutcome` | `run()` / `resume()` 終了時 | interrupted フラグのみ(監査用) |
| `ConfigChanged` | `set_*` メソッド群 | config を更新 |
## Session ラッパー
```rust
pub struct Session<C: LlmClient, St: Store> {
pub worker: Worker<C, Mutable>,
store: St,
session_id: SessionId,
}
```
- `Session::new()` — SessionStart を書き込み
- `Session::run()` — Worker::run() の前後で history を比較、差分をログ記録
- `Session::resume()` — 同上
- `Session::restore()` — ログを読み込み、状態を集約して Worker を再構築
- `Session::fork()` — 現在の history をシードにした新セッションを作成
- `Session::fork_at()` — 任意のログ地点から分岐
## Store trait
```rust
pub trait Store: Send + Sync {
fn append(&self, id: SessionId, entry: &LogEntry) -> impl Future<...> + Send;
fn read_all(&self, id: SessionId) -> impl Future<...> + Send;
fn list_sessions(&self) -> impl Future<...> + Send;
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> impl Future<...> + Send;
fn exists(&self, id: SessionId) -> impl Future<...> + Send;
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> impl Future<...> + Send;
}
```
初期実装は `FsStore`(ファイルシステム JSONL)。RPITIT 使用、`async_trait` 不要。
+8 -245
View File
@@ -1,13 +1,6 @@
# Pod Protocol 仕様
# Pod Protocol
## 概要
Pod の制御・監視に使う JSONL ベースのメッセージプロトコル。
トランスポートに依存しない。CLI は Pod を直接制御し、daemon は Unix socket 上でこのプロトコルを中継する。
- **フレーミング**: 1行 = 1 JSON オブジェクト(`\n` 区切り)
- **方向**: 双方向。クライアントはメソッドを送信し、Pod はイベントを emit する
Pod の制御・監視に使う JSONL ベースのメッセージプロトコル。トランスポートに依存しない。
```
CLI → Pod Protocol (直接呼び出し)
@@ -15,242 +8,12 @@ Native App → Pod Protocol (直接呼び出し)
Web → 中央バックエンド → daemon (Unix socket) → Pod Protocol
```
## 設計原則
## 設計判断
- リクエストとレスポンスの紐付けはしないPod は1つであり、Pod の状態遷移(イベント)を見れば何が起きているか分かる
- イベントは全リスナーに broadcast される。読み取り専用の監視も、操作側も同じストリームを受け取る
- 操作の競合は先勝ちrun 中に別の run が来たらエラーイベントを返す
- **リクエストとレスポンスの紐付けはしない**: Pod は1つであり、Pod の状態遷移(イベント)を見れば何が起きているか分かる
- **イベントは全リスナーに broadcast**: 読み取り専用の監視も、操作側も同じストリームを受け取る
- **操作の競合は先勝ち**: run 中に別の run が来たらエラーイベントを返す
## メッセージ形式
## daemon
### クライアント → Pod(メソッド)
```json
{"method": "<name>", "params": {<...>}}
```
`params` はメソッドごとに異なる。省略可能な場合は `params` フィールド自体を省略できる。
### Pod → クライアント(イベント)
```json
{"event": "<name>", "data": {<...>}}
```
全リスナーに broadcast される。
## メソッド一覧
### `run`
ユーザー入力を送信し、LLM ターンを開始する。
```json
{"method": "run", "params": {"input": "What is the capital of France?"}}
```
Pod が既に実行中の場合、エラーイベントが返る。
### `resume`
Paused 状態から再開する。
```json
{"method": "resume"}
```
### `cancel`
実行中のターンをキャンセルする。
```json
{"method": "cancel"}
```
### `get_status`
Pod の現在の状態を要求する。応答は `status` イベントとして返る。
```json
{"method": "get_status"}
```
### `get_history`
会話履歴を要求する。応答は `history` イベントとして返る。
```json
{"method": "get_history"}
```
## イベント一覧
### ターン制御
#### `turn_start`
LLM ターンの開始。
```json
{"event": "turn_start", "data": {"turn": 1}}
```
#### `turn_end`
LLM ターンの完了。
```json
{"event": "turn_end", "data": {"turn": 1, "result": "finished"}}
```
`result`: `"finished"` | `"paused"`
### ストリーミング
#### `text_delta`
テキスト応答の差分。
```json
{"event": "text_delta", "data": {"text": "The capital"}}
```
#### `text_done`
テキストブロックの完了。全文を含む。
```json
{"event": "text_done", "data": {"text": "The capital of France is Paris."}}
```
#### `thinking_delta`
思考プロセスの差分(extended thinking 対応モデル)。
```json
{"event": "thinking_delta", "data": {"text": "Let me consider..."}}
```
#### `thinking_done`
思考ブロックの完了。
```json
{"event": "thinking_done", "data": {"text": "..."}}
```
### ツール
#### `tool_call_start`
ツール呼び出しの開始。
```json
{"event": "tool_call_start", "data": {"id": "call_123", "name": "search"}}
```
#### `tool_call_args_delta`
ツール引数の JSON 差分(ストリーミング中)。
```json
{"event": "tool_call_args_delta", "data": {"id": "call_123", "json": "{\"query\":"}}
```
#### `tool_call_done`
ツール呼び出しの引数確定。
```json
{"event": "tool_call_done", "data": {"id": "call_123", "name": "search", "arguments": "{\"query\": \"Paris\"}"}}
```
#### `tool_result`
ツール実行結果。
```json
{"event": "tool_result", "data": {"id": "call_123", "output": "Paris is the capital...", "is_error": false}}
```
### 状態
#### `status`
`get_status` への応答、または状態変化時に送信。
```json
{"event": "status", "data": {"state": "idle", "session_id": "019d6e91-...", "pod_name": "hello-pod"}}
```
`state`: `"idle"` | `"running"` | `"paused"`
#### `history`
`get_history` への応答。
```json
{"event": "history", "data": {"items": [...]}}
```
`items` は llm-worker の `Item` 配列をそのまま JSON シリアライズしたもの。
### メタ
#### `usage`
トークン使用量。
```json
{"event": "usage", "data": {"input_tokens": 25, "output_tokens": 150}}
```
#### `error`
エラー通知。
```json
{"event": "error", "data": {"code": "already_running", "message": "Pod is already executing a turn"}}
```
エラーコード:
- `already_running` — run 中に run が来た
- `not_running` — run していないのに resume/cancel が来た
- `not_paused` — paused でないのに resume が来た
- `provider_error` — LLM プロバイダからのエラー
- `tool_error` — ツール実行エラー
- `internal` — 内部エラー
## リスナーのライフサイクル
1. リスナーが登録される(直接呼び出しなら関数登録、daemon 経由なら socket 接続)
2. 登録直後から Pod のイベントが流れ始める(購読手続き不要)
3. クライアントはメソッドを任意のタイミングで送信できる
4. リスナーの解除は登録解除または接続切断で行う
## トランスポート: daemon (Unix socket)
daemon は Pod Protocol を Unix domain socket 上で中継する薄い層。
- クライアントが socket に接続するとリスナーとして登録される
- メソッドは socket 経由で Pod に転送される
- 切断時にリスナーリストから除外するだけでクリーンアップ完了
## イベントと llm-worker の対応
| イベント | llm-worker ソース |
|---------------|-----------------|
| `turn_start` | Subscriber `on_turn_start` |
| `turn_end` | Subscriber `on_turn_end` + `WorkerResult` |
| `text_delta` | `TextBlockEvent::Delta` |
| `text_done` | Subscriber `on_text_complete` |
| `thinking_delta` | `ThinkingBlockEvent::Delta` |
| `thinking_done` | `ThinkingBlockEvent::Stop` |
| `tool_call_start` | `ToolUseBlockEvent::Start` |
| `tool_call_args_delta` | `ToolUseBlockEvent::InputJsonDelta` |
| `tool_call_done` | Subscriber `on_tool_call_complete` |
| `tool_result` | `PostToolCall` hook |
| `usage` | `UsageEvent` |
| `error` | `ErrorEvent` / `WorkerError` |
| `status` | Pod 状態(Pod 層が管理) |
| `history` | `Worker::history()` |
daemon は Pod Protocol を Unix domain socket 上で中継する薄い層。接続=リスナー登録、切断=リスナー解除。それだけ。
-158
View File
@@ -1,158 +0,0 @@
# テスト Fixture 仕様
## 概要
テスト用 fixture は、実 API のストリーミング応答を JSONL 形式で録画したファイル。
`MockLlmClient::from_fixture()` でロードし、API キー不要・決定的なテスト実行を実現する。
## ファイル形式
```
{メタデータ行: JSON}
{イベント行: JSON}
{イベント行: JSON}
...
```
- **1行目**: メタデータ (`timestamp`, `model`, `description`)
- **2行目以降**: 録画イベント (`elapsed_ms`, `event_type`, `data`)
- `data` フィールドに `Event` の JSON 文字列が入る
## ファイル配置
```
crates/llm-worker/tests/fixtures/
anthropic/
simple_text.jsonl
tool_call.jsonl
long_text.jsonl
openai/
simple_text.jsonl
tool_call.jsonl
long_text.jsonl
gemini/
simple_text.jsonl
tool_call.jsonl
long_text.jsonl
ollama/
simple_text.jsonl
tool_call.jsonl
long_text.jsonl
```
## シナリオ定義
### simple_text
単純なテキスト応答。
| 項目 | 値 |
|---|---|
| ファイル名 | `simple_text.jsonl` |
| system prompt | `"You are a helpful assistant. Be very concise."` |
| user message | `"Say hello in one word."` |
| max_tokens | 50 |
| ツール | なし |
**期待パターン**:
- `BlockStart(Text)` が1つ以上
- `BlockDelta(Text)` が1つ以上
- `BlockStop(Text)` が1つ以上
- 応答が短い(1単語程度)
**用途**: 基本的なストリーミング動作、Timeline テキスト収集、Worker の単純な run 完了
### tool_call
ツール呼び出しを含む応答。
| 項目 | 値 |
|---|---|
| ファイル名 | `tool_call.jsonl` |
| system prompt | `"You are a helpful assistant. Use tools when appropriate."` |
| user message | `"What's the weather in Tokyo? Use the get_weather tool."` |
| max_tokens | 200 |
| ツール | `get_weather(city: string)` |
**期待パターン**:
- `BlockStart(ToolUse)` を含む
- ToolUse ブロック内に `tool_call_id`, `name: "get_weather"` がある
- tool input JSON に `"city"` キーを含む
**用途**: ToolCallCollector、Worker のツール実行フロー、Session の ToolResults ログ記録
### long_text
長文テキスト応答。
| 項目 | 値 |
|---|---|
| ファイル名 | `long_text.jsonl` |
| system prompt | `"You are a creative writer."` |
| user message | `"Write a short story about a robot discovering a garden. It should be at least 300 words."` |
| max_tokens | 1000 |
| ツール | なし |
**期待パターン**:
- `BlockDelta(Text)` が複数(ストリーミングチャンク)
- 最終テキストが 300 語以上
**用途**: ストリーミングの分割配信検証、Subscriber のデルタ受信テスト
## 共通検証項目
全 fixture に対して以下を検証する(`assert_*` ヘルパー関数群):
- `assert_events_deserialize` — 全イベントが `Event` にデシリアライズできる
- `assert_event_sequence` — BlockStart → BlockDelta → BlockStop の基本シーケンス
- `assert_usage_tokens``Usage` イベントが含まれる
- `assert_timeline_integration` — Timeline に流してテキスト収集できる
## 録画手順
### 前提
- API キーが環境変数に設定されていること
- `crates/llm-worker` ディレクトリで実行
### コマンド
```bash
# 単一シナリオ録画
ANTHROPIC_API_KEY=... cargo run --example record_test_fixtures -- -s simple_text
# 全シナリオ録画
ANTHROPIC_API_KEY=... cargo run --example record_test_fixtures -- --all
# プロバイダー指定
OPENAI_API_KEY=... cargo run --example record_test_fixtures -- --all -c openai
GEMINI_API_KEY=... cargo run --example record_test_fixtures -- --all -c gemini
# モデル指定
ANTHROPIC_API_KEY=... cargo run --example record_test_fixtures -- --all -m claude-sonnet-4-20250514
```
### シナリオ定義の場所
`crates/llm-worker/examples/record_test_fixtures/scenarios.rs`
新しいシナリオを追加する場合はこのファイルに `TestScenario` を追加し、
`scenarios()` 関数の返り値に含める。
## 録画後の確認チェックリスト
録画後、テストに組み込む前に以下を手動確認する:
- [ ] JSONL の各行が valid JSON か(`jq . < fixture.jsonl` で確認)
- [ ] 1行目にメタデータ(`timestamp`, `model`, `description`)が入っているか
- [ ] simple_text: `BlockStart``BlockDelta``BlockStop` シーケンスがあるか
- [ ] tool_call: `BlockStart``"block_type":"ToolUse"` を含むか
- [ ] long_text: `BlockDelta` が複数行あるか(ストリーミング分割の確認)
- [ ] 各 fixture に `Usage` イベントが含まれるか
- [ ] エラーイベントが混入していないか
## 注意事項
- fixture は API の応答に依存するため、モデルバージョンアップで再録画が必要になることがある
- 録画の自動化(CI での定期録画等)は行わない。手動実行 + 目視確認のフロー
- fixture が存在しない場合、対応するテストは skip される(`if !fixture_path.exists() { return; }` パターン)