init
This commit is contained in:
commit
14db66e5b0
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.direnv
|
||||
35
AGENTS.md
Normal file
35
AGENTS.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# llm-worker-rs 開発instruction
|
||||
|
||||
## パッケージ管理ルール
|
||||
|
||||
- クレートに依存関係を追加・更新する際は必ず
|
||||
`cargo`コマンドを使い、`Cargo.toml`を直接手で書き換えず、必ずコマンド経由で管理すること。
|
||||
|
||||
## worker-types
|
||||
|
||||
`worker-types` クレートには次の条件を満たす型だけを置く。
|
||||
|
||||
1. **共有セマンティクスの源泉**
|
||||
- ランタイム(`worker`)、proc-macro(`worker-macros`)、外部利用者のすべてで同じ定義を共有したい値型。
|
||||
- 例: `BlockId`, `ProviderEvent`, `ToolArgumentsDelta` などイベント/DTO群。
|
||||
|
||||
2. **シリアライズ境界を越えるもの**
|
||||
- serde経由でプロセス外へ渡したり、APIレスポンスとして公開するもの。
|
||||
- ロジックを持たない純粋なデータキャリアに限定する。
|
||||
|
||||
3. **依存の最小化が必要な型**
|
||||
- `serde`, `serde_json` 程度の軽量依存で収まる。
|
||||
|
||||
4. **マクロが直接参照する型**
|
||||
- 属性/derive/proc-macro が型に対してコード生成する場合は `worker-macros` ->
|
||||
`worker-types` の単方向依存を維持するため、対象型を `worker-types` に置く。
|
||||
|
||||
5. **副作用を伴わないこと**
|
||||
- `worker-types` 内では I/O・状態保持・スレッド操作などの副作用を禁止。
|
||||
- 振る舞いを持つ場合でも `impl`
|
||||
は純粋な計算か軽量ユーティリティのみに留める。
|
||||
|
||||
この基準に当てはまらない型(例えばクライアント状態管理、エラー型で追加依存が必要、プロバイダ固有ロジックなど)は
|
||||
`worker` クレート側に配置し、どうしても公開が必要なら `worker`
|
||||
経由で再エクスポートする。 何にせよ、`worker` ->
|
||||
`worker-types`の片方向依存を維持すること。
|
||||
147
Cargo.lock
generated
Normal file
147
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "worker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"worker-macros",
|
||||
"worker-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worker-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"worker-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worker-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5"
|
||||
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"worker",
|
||||
"worker-types",
|
||||
"worker-macros",
|
||||
]
|
||||
26
docs/research/2026-01-02-llm-streaming.md
Normal file
26
docs/research/2026-01-02-llm-streaming.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 2026-01-02 LLM Streaming & Hooks Research
|
||||
|
||||
## Summary Table
|
||||
| Topic | Key Takeaways | Sources |
|
||||
| --- | --- | --- |
|
||||
| Fine-grained tool streaming | Anthropic beta header `fine-grained-tool-streaming-2025-05-14` streams tool parameters without intermediate JSON validation; reduces latency but may emit invalid/partial JSON that callers must sanitize. | Anthropic Docs – Fine-grained tool streaming (https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/fine-grained-tool-streaming) [turn1search0]; AWS Bedrock Anthropic tool-use reference (https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-tool-use.html) [turn1search5]; Anthropic release notes (https://docs.anthropic.com/en/release-notes/api) [turn1search3]
|
||||
| Streaming SSE events | Anthropic SSE stream emits typed events (`message_start`, `content_block_start`, `content_block_delta`, etc.) that clients must map to internal state machines for deterministic playback. | Anthropic Streaming Messages (https://docs.anthropic.com/en/docs/build-with-claude/streaming) [turn1search6]
|
||||
| Tool streaming ergonomics | LangChain Anthropic integration exposes `betas=["fine-grained-tool-streaming-2025-05-14"]` and warns about invalid JSON, reinforcing need for resilient parsers. | LangChain Anthropic integration guide (https://docs.langchain.com/oss/python/integrations/chat/anthropic) [turn1search8]
|
||||
| Hook architectures | Claude Code hook lifecycle (SessionStart, UserPromptSubmit, Tool Use, etc.) keeps hooks non-blocking, context-injecting, and failure-isolated—useful template for worker hooks/routers. | Claude-Mem hook architecture overview (https://docs.claude-mem.ai/hooks-architecture) [turn0search0]; Claude Blog – configuring hooks (https://claude.com/blog/how-to-configure-hooks) [turn0search2]
|
||||
|
||||
## Detailed Notes
|
||||
|
||||
### Fine-grained Tool Streaming
|
||||
- Anthropic’s beta header `fine-grained-tool-streaming-2025-05-14` enables parameter streams, shrinking first-byte latency from ~15s to ~3s in their example. Clients must prepare for partial JSON and wrap invalid payloads before echoing them back. [turn1search0]
|
||||
- AWS Bedrock mirrors the same header, confirming availability on Claude Sonnet 4.5/4 and Opus 4. Their docs explicitly caution about invalid/partial JSON and show the request schema. [turn1search5]
|
||||
- Anthropic’s June 11, 2025 release notes document the beta launch, signaling freshness and likely API stability requirements. [turn1search3]
|
||||
|
||||
### SSE Event Model
|
||||
- Anthropic exposes SSE event names plus JSON `type` fields; implementers should parse events like `content_block_start`, `content_block_delta`, and `message_stop` to drive a deterministic timeline. This justifies a dedicated timeline router/state machine as in the spec. [turn1search6]
|
||||
|
||||
### Tool Streaming Ergonomics in SDKs
|
||||
- LangChain’s Anthropic integration demonstrates how third-party SDKs surface the beta header and reiterates the need for error handling when incomplete JSON arrives because `max_tokens` can stop a stream mid-parameter. This informs library-level abstractions for `scheme` and `llm_client` layers. [turn1search8]
|
||||
|
||||
### Hook / Lifecycle Design Patterns
|
||||
- Claude-Mem’s architecture treats hooks as lifecycle-triggered closures that must stay non-blocking, degrade gracefully, and respect security constraints (frozen configs, permission prompts). This maps closely to the proposed `Tools/Hooks` system. [turn0search0]
|
||||
- Claude’s official hook configuration guide enumerates eight hook types (PreToolUse, PostToolUse, PermissionRequest, SessionStart, Stop, etc.) and their contexts, reinforcing the need for a trait/macro system to statically describe hooks and route events. [turn0search2]
|
||||
31
docs/research/2026-01-02-provider-event-specs.md
Normal file
31
docs/research/2026-01-02-provider-event-specs.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# 2026-01-02 Provider Event & Tool Streaming Specs
|
||||
|
||||
## Snapshot
|
||||
| Provider | Stream Transport | Event/Deltas | Tool Call Streaming | References |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| OpenAI Responses API | SSE (`stream=true`) with typed semantic events | `response.created`, `response.output_text.delta/done`, `response.function_call_arguments.delta/done`, `response.refusal.*`, `response.code_interpreter.*`, etc. | Tool/function arguments stream as JSON string deltas; supports MCP tool calls and built-ins. | OpenAI Streaming guide & API reference (2025-12) |
|
||||
| Anthropic Messages API | SSE (`stream=true`) enumerating `message_*` + `content_block_*` events | Content blocks typed (`text_delta`, `input_json_delta`, `thinking_delta`, `signature_delta`, tool results). `ping` and `error` events possible. | Fine-grained tool streaming via beta header `anthropic-beta: fine-grained-tool-streaming-2025-05-14`; tool inputs stream as `partial_json`. | Anthropic Streaming docs (2025-08) + Fine-grained Tool Streaming beta (2025-05) |
|
||||
| Google Gemini API | SSE via `:streamGenerateContent?alt=sse` returning repeated `GenerateContentResponse` objects | Each chunk is a partial `candidates[]` payload; structured output streaming returns partial JSON; Live API adds WebSocket session events. | Function declarations via `tools.functionDeclarations`. `streamFunctionCallArguments=true` yields `partialArgs` records with `jsonPath` + `willContinue`. | Gemini REST docs + Function Calling guide + Vertex AI streaming args (updated 2025-12) |
|
||||
|
||||
## OpenAI Responses API (2025-12 docs)
|
||||
- **Lifecycle events**: `response.created`, `response.queued`, `response.in_progress`, `response.completed`, `response.failed`, `error`. Each emits `sequence_number` for ordering.
|
||||
- **Content events**: `response.output_text.delta/done`, `response.output_text.annotation.added`, `response.refusal.delta/done`, plus `response.output_item.added/done` for multi-part outputs.
|
||||
- **Tool events**: `response.function_call_arguments.delta/done`, `response.file_search_call.*`, `response.code_interpreter_call.*`, `response.custom_tool_call_input.delta`, `response.mcp_call_arguments.delta/done` for MCP connectors.
|
||||
- **Design takeaways**: timeline layer must preserve ordering by `sequence_number`, correlate `item_id`/`output_index`, and buffer partial JSON until `.done`. Provide parsing helpers for MCP/custom tool deltas and code interpreter outputs to unify built-in + custom tools.
|
||||
- **Primary sources**: OpenAI Streaming Responses Guide + Streaming Events API reference (retrieved Jan 2 2026).
|
||||
|
||||
## Anthropic Messages API
|
||||
- **Event flow**: Always begins with `message_start`, then repeated blocks of (`content_block_start` → `content_block_delta`* → `content_block_stop`), optional `message_delta`, and `message_stop`. `ping` and `error` events may interleave.
|
||||
- **Content block delta types**: `text_delta`, `input_json_delta` (tool inputs), `thinking_delta`, `signature_delta` (integrity for thinking), plus specialized tool result payloads (e.g., `code_execution_tool_result`).
|
||||
- **Fine-grained tool streaming**: Beta header `fine-grained-tool-streaming-2025-05-14` streams tool parameters chunk-by-chunk without JSON validation; clients must handle invalid/partial JSON when `max_tokens` stop reason fires mid-argument. Recommended strategy: accumulate raw string, optionally wrap invalid JSON before echoing back.
|
||||
- **Operational cautions**: Tool input streaming may pause between keys (docs warn of delays). Provide timeouts/heartbeat detection for UIs (issue reports note >3 min stalls with no `ping`).
|
||||
- **Primary sources**: Anthropic Streaming Messages guide & Fine-grained Tool Streaming beta docs (retrieved Jan 2 2026).
|
||||
|
||||
## Google Gemini API
|
||||
- **REST streaming**: `POST https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent?alt=sse` returns SSE where each `data:` line is a partial `GenerateContentResponse` (with `candidates`, `usageMetadata`, `modelVersion`). Chrome DevDoc example shows chunked `text` fields.
|
||||
- **Structured output streaming**: When `response_mime_type=application/json`, streamed chunks are valid partial JSON strings that concatenate to the final object.
|
||||
- **Function calling**: Tools defined via `functionDeclarations`; `function_calling_config` modes `AUTO/ANY/NONE`. Latest docs confirm JSON Schema + Pydantic/Zod compatibility (2025-12 update) and compositional calls.
|
||||
- **Function call argument streaming**: Set `toolConfig.functionCallingConfig.streamFunctionCallArguments=true`; responses include `functionCall.partialArgs` with `jsonPath`, `delta` values (string/number/bool/null) and `willContinue`. Vertex AI content schema documents `PartialArg` message for decoding.
|
||||
- **Transports**: SSE for REST, WebSockets for Live API sessions (bidirectional audio/text). Need to abstract both.
|
||||
- **Primary sources**: Gemini API overview + streamGenerateContent reference + function calling guide + Vertex AI streaming arguments doc + Chrome streaming example (retrieved Jan 2 2026).
|
||||
|
||||
68
docs/spec/basis.md
Normal file
68
docs/spec/basis.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# llm_worker_rs
|
||||
|
||||
LLMを用いたワーカーを作成する小型のSDK・ライブラリ。
|
||||
|
||||
ツールの定義や呼び出し、コンテキストの管理などを責務にもつ。
|
||||
|
||||
## 用語定義
|
||||
|
||||
- **ブロック (Block)**:
|
||||
モデルが「開始→デルタ→終了」で囲む一塊の出力。OpenAIの`response.output_item`やAnthropicの`content_block`、Geminiの`candidates[].content.parts`などが該当する。
|
||||
- **メタイベント (Meta Event)**:
|
||||
`ping`や`usage`など、ブロックに属さない補助イベント群。ステータス更新やハートビートとして処理される。
|
||||
|
||||
以降の仕様では「ブロック」を上記の生成・ツール単位の総称として扱う。
|
||||
|
||||
## アプローチ
|
||||
|
||||
module構成概念図
|
||||
|
||||
```
|
||||
worker
|
||||
├── context
|
||||
├── llm_client
|
||||
│ ├── scheme
|
||||
│ │ └── openai, anthropic, gemini // APIスキーマ
|
||||
│ ├── events
|
||||
│ ├── providers
|
||||
│ │ └── anthropic, googleai, ollama, etc... // プロバイダ
|
||||
│ └── timeline
|
||||
```
|
||||
|
||||
OpenAI互換のプロバイダでスキーマを使い回せるよう、schemeとプロバイダモジュールは分離されている
|
||||
|
||||
### scheme層
|
||||
|
||||
単純な変換を責務とするスキーマを定義する。
|
||||
|
||||
- リクエスト変換: SystemPrompt + Context + Tools... → プロバイダ固有のリクエストJSON
|
||||
- レスポンス変換: Raw stream → 型付きイベント構造体のストリーム
|
||||
|
||||
各APIスキーマ(≠プロバイダ)ごとに実装を持ち、APIスキーマに準じたパース/バリデーションを行う。
|
||||
|
||||
### llm_client(providers)層
|
||||
|
||||
providersにリクエストを送信し、差異が吸収され統一された`Event`ストリームを出力する。
|
||||
|
||||
ストリーミング中のバッファリング(ToolArgumentsの累積等)もこの層で行う。
|
||||
|
||||
### timeline層
|
||||
|
||||
イベントストリームの状態を管理する。レスポンスをステートマシンで制約し、イベントをハンドラーに渡す。
|
||||
|
||||
このTimelineに基づき、Workerはツール実行・フックを行う。
|
||||
|
||||
イベントストリームを扱う目標に、Text/Input JSON/Thinkingのdeltaと、[細粒度ツールストリーミング](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming)を含む、deltaの低遅延・リアルタイム処理がある。
|
||||
主要なドキュメント:[https://platform.claude.com/docs/en/build-with-claude/streaming#content-block-delta-types]
|
||||
|
||||
TODO: Workerをユーザーが扱うUIとした際の、Timelineの露出のさせ方
|
||||
|
||||
## Tools/Hooks
|
||||
|
||||
各種プロバイダのLLMのAPI仕様として存在するTool Callや、Function CallingをToolsとして統一的に扱ってる。また、Claude Codeに存在するようなHooksと同じような機能を搭載する。
|
||||
|
||||
これらはtraitとして表現され、クロージャを持ち、イベントのストリーム中に自動的に呼び出される。
|
||||
|
||||
Tools/Hooksを定義する為のProcedural macroを提供する。
|
||||
|
||||
TODO: Tool定義と合わせたTimeline Handlerの定義方法(特定のツールとセットになったHandlerの考慮)
|
||||
256
docs/spec/timeline_design.md
Normal file
256
docs/spec/timeline_design.md
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# Timeline層設計メモ
|
||||
|
||||
## 目的
|
||||
|
||||
- OpenAI / Anthropic / Gemini
|
||||
のストリーミングイベントを単一の抽象レイヤーに正規化し、LLMクライアントの状態遷移を制御する。
|
||||
- イベント単位処理(Meta系など)とブロック単位処理(テキスト/Thinking/ToolCall)を同一のパイプラインで扱えるようにする。
|
||||
|
||||
## 要件
|
||||
|
||||
イベントストリームに対して直接的にループ処理をしようとすると発生する煩雑な状態管理を避ける。
|
||||
|
||||
イベントをloop+matchで処理をするような手段を取ると、テキストのデルタの更新先や、完了タイミングなどの状態管理が必要になる。
|
||||
また、コンテンツブロックに対する単純なイテレータではping/usageなどの単発イベントを同期的に処理することができない。
|
||||
|
||||
- Meta系イベントの即時処理 (ブロック内部イベントと順序が前後しないようにする)
|
||||
- ブロック開始/差分/終了でスコープを保持する
|
||||
- 型安全なハンドラー
|
||||
- blockでキャッチするdeltaについて、 Text/Input JSON/Thinking等、
|
||||
ブロックに即したイベントの型が必要。
|
||||
- エラーの適切な制御
|
||||
|
||||
TODO:toolのjsonをキャッチする際、定義したToolが期待するjsonschemaに合致しているかのバリデーションはTimeline層の責務か?
|
||||
|
||||
## Memo
|
||||
|
||||
- [Blockの定義](basis.md#用語定義)
|
||||
- ブロックを処理するHandlerで保持するコンテキストについて、LLMで用いられるコンテキストと混同を避けるために「スコープ」と呼称する。
|
||||
- ブロックは常に一つである前提。複数のブロックが同時に存在することは無いため。
|
||||
|
||||
## イベントモデル
|
||||
|
||||
前提:`llm_client`層は各プロバイダのストリーミングレスポンスを正規化し、**フラットなEvent列挙**として出力する。
|
||||
|
||||
```rust
|
||||
pub enum Event {
|
||||
// Meta events (not tied to a block)
|
||||
Ping(PingEvent),
|
||||
Usage(UsageEvent),
|
||||
Status(StatusEvent),
|
||||
Error(ErrorEvent),
|
||||
|
||||
// Block lifecycle events
|
||||
BlockStart(BlockStart),
|
||||
BlockDelta(BlockDelta),
|
||||
BlockStop(BlockStop),
|
||||
BlockAbort(BlockAbort),
|
||||
}
|
||||
```
|
||||
|
||||
-> Timelineがブロック構造化を担う
|
||||
|
||||
### 設計方針
|
||||
|
||||
- 目的: アプリケーションとの連携を楽にする
|
||||
|
||||
状態共有/非同期: Handler間の状態共有はアプリケーション側の責務、アプリケーション側で非同期化可能な設計
|
||||
|
||||
複数のHandlerに対し、登録順にディスパッチする。Handlerは後続Handlerへのイベントを改変しない。
|
||||
|
||||
## TimelineとKind/Handler
|
||||
|
||||
Timelineは`Event`ストリームを受け取り、登録された`Handler<K: Kind>`にディスパッチする。
|
||||
|
||||
### Kind
|
||||
|
||||
`Kind`はイベント型のみを定義する。スコープはHandler側で定義するため、同じKindに対して異なるスコープを持つHandlerを登録できる。
|
||||
|
||||
```rust
|
||||
pub trait Kind {
|
||||
type Event;
|
||||
}
|
||||
```
|
||||
|
||||
### Handler
|
||||
|
||||
`Handler`はKindに対する処理を定義し、自身のスコープ型も決定する。
|
||||
|
||||
```rust
|
||||
pub trait Handler<K: Kind> {
|
||||
type Scope: Default;
|
||||
|
||||
fn on_event(&mut self, scope: &mut Self::Scope, event: &K::Event);
|
||||
}
|
||||
```
|
||||
|
||||
- `Kind`によって受け取るイベント型が決定される
|
||||
- `Handler::Scope`によってHandler固有のスコープ型が決定される
|
||||
- Meta系とBlock系を統一的に扱える
|
||||
|
||||
### Meta系Kind
|
||||
|
||||
スコープ不要の単発イベント:
|
||||
|
||||
```rust
|
||||
pub struct UsageKind;
|
||||
impl Kind for UsageKind {
|
||||
type Event = UsageEvent;
|
||||
}
|
||||
|
||||
pub struct PingKind;
|
||||
impl Kind for PingKind {
|
||||
type Event = PingEvent;
|
||||
}
|
||||
|
||||
// 使用例
|
||||
struct UsageAccumulator { total_tokens: u64 }
|
||||
impl Handler<UsageKind> for UsageAccumulator {
|
||||
type Scope = (); // スコープ不要
|
||||
|
||||
fn on_event(&mut self, _scope: &mut (), usage: &UsageEvent) {
|
||||
self.total_tokens += usage.total_tokens.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Block系Kind
|
||||
|
||||
ライフサイクル(Start/Delta/Stop)を持つ。スコープはHandler側で定義:
|
||||
|
||||
```rust
|
||||
pub struct TextBlockKind;
|
||||
impl Kind for TextBlockKind {
|
||||
type Event = TextBlockEvent;
|
||||
}
|
||||
|
||||
pub enum TextBlockEvent<'a> {
|
||||
Start(&'a BlockStart),
|
||||
Delta(&'a str),
|
||||
Stop(&'a BlockStop),
|
||||
}
|
||||
|
||||
// 使用例1: デルタを即時出力(スコープ不要)
|
||||
struct PrintHandler;
|
||||
impl Handler<TextBlockKind> for PrintHandler {
|
||||
type Scope = ();
|
||||
|
||||
fn on_event(&mut self, _scope: &mut (), event: &TextBlockEvent) {
|
||||
if let TextBlockEvent::Delta(s) = event {
|
||||
print!("{}", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用例2: テキストを蓄積して収集(Stringをスコープとして利用)
|
||||
struct TextCollector { results: Vec<String> }
|
||||
impl Handler<TextBlockKind> for TextCollector {
|
||||
type Scope = String; // bufferとして使用
|
||||
|
||||
fn on_event(&mut self, buffer: &mut String, event: &TextBlockEvent) {
|
||||
match event {
|
||||
TextBlockEvent::Start(_) => {}
|
||||
TextBlockEvent::Delta(s) => buffer.push_str(s),
|
||||
TextBlockEvent::Stop(_) => {
|
||||
self.results.push(std::mem::take(buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timelineの責務
|
||||
|
||||
1. `Event`ストリームを受信
|
||||
2. Block系イベント(BlockStart/Delta/Stop)をBlockKindごとのライフサイクルイベントに変換
|
||||
3. 各Handlerごとのスコープの生成・管理(BlockStart時に生成、BlockStop/Abort時に破棄)
|
||||
4. 登録されたHandlerへの登録順ディスパッチ
|
||||
|
||||
### Handlerの型消去
|
||||
|
||||
各Handlerは独自のScope型を持つため、Timelineで保持するには型消去が必要:
|
||||
|
||||
```rust
|
||||
// 型消去されたHandler trait
|
||||
trait ErasedHandler<K: Kind> {
|
||||
fn dispatch(&mut self, event: &K::Event);
|
||||
fn start_scope(&mut self); // Scope生成
|
||||
fn end_scope(&mut self); // Scope破棄
|
||||
}
|
||||
|
||||
// Handler<K>からErasedHandler<K>へのラッパー
|
||||
struct HandlerWrapper<H, K>
|
||||
where
|
||||
H: Handler<K>,
|
||||
K: Kind,
|
||||
{
|
||||
handler: H,
|
||||
scope: Option<H::Scope>, // Block中のみSome
|
||||
_kind: PhantomData<K>,
|
||||
}
|
||||
|
||||
impl<H, K> ErasedHandler<K> for HandlerWrapper<H, K>
|
||||
where
|
||||
H: Handler<K>,
|
||||
K: Kind,
|
||||
{
|
||||
fn dispatch(&mut self, event: &K::Event) {
|
||||
if let Some(scope) = &mut self.scope {
|
||||
self.handler.on_event(scope, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_scope(&mut self) {
|
||||
self.scope = Some(H::Scope::default());
|
||||
}
|
||||
|
||||
fn end_scope(&mut self) {
|
||||
self.scope = None;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timelineのディスパッチ
|
||||
|
||||
```rust
|
||||
impl Timeline {
|
||||
pub fn dispatch(&mut self, event: &Event) {
|
||||
match event {
|
||||
// Meta系: 即時ディスパッチ(登録順)
|
||||
Event::Usage(u) => self.dispatch_to::<UsageKind>(u),
|
||||
Event::Ping(p) => self.dispatch_to::<PingKind>(p),
|
||||
|
||||
// Block系: スコープ管理しながらディスパッチ
|
||||
Event::BlockStart(s) => self.handle_block_start(s),
|
||||
Event::BlockDelta(d) => self.handle_block_delta(d),
|
||||
Event::BlockStop(s) => self.handle_block_stop(s),
|
||||
Event::BlockAbort(a) => self.handle_block_abort(a),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_block_start(&mut self, start: &BlockStart) {
|
||||
// 該当Kind の全Handlerに対してスコープ生成
|
||||
for handler in self.handlers_for_kind(start.kind()) {
|
||||
handler.start_scope();
|
||||
handler.dispatch(&start.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_block_stop(&mut self, stop: &BlockStop) {
|
||||
for handler in self.handlers_for_kind(stop.kind()) {
|
||||
handler.dispatch(&stop.into());
|
||||
handler.end_scope();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 期待効果
|
||||
|
||||
- **統一インターフェース**: Meta系もBlock系も`Handler<K: Kind>`で統一
|
||||
- **型安全**: `Kind`によってイベント型がコンパイル時に決定、`Handler::Scope`によってHandler固有のスコープ型が決定
|
||||
- **スコープの柔軟性**: 同一Kindに対して異なるScopeを持つ複数Handlerを登録可能
|
||||
- **責務分離**: llm_client層はフラットなEvent出力、Timeline層がブロック構造化
|
||||
- **スコープ管理の自動化**: Handlerは自前でスコープ保持を意識せずに済む
|
||||
77
flake.lock
Normal file
77
flake.lock
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767116409,
|
||||
"narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cad22e7d996aea55ecab064e84834289143e44a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
31
flake.nix
Normal file
31
flake.nix
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
flake-compat.url = "github:edolstra/flake-compat";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
nixfmt
|
||||
deno
|
||||
git
|
||||
rustc
|
||||
cargo
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
pkg-config
|
||||
openssl
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user