feat: OpenResponses

This commit is contained in:
2026-02-19 17:37:29 +09:00
parent 6c43ac9969
commit 3c62970967
15 changed files with 2066 additions and 673 deletions
+83
View File
@@ -0,0 +1,83 @@
# Worker API/DSL 実装計画
## 目的
- [Open Responses](https://www.openresponses.org)(以後"OR")に準拠した正規化を前提に、
Item/Part の2段スコープを扱える Worker API を設計する。
- APIの煩雑化を防ぐため、worker.on_xxx として公開するのを避けつつ、
Text/Thinking/Tool など型の違いを静的に扱える DSL を提供する。
## 方針
- 内部は Timeline が Event を正規化し、Item/Part/Meta
を単一ストリームとして扱う。
- API では Item/Part 型ごとに ctx を持てるようにし、DSL
で記述の冗長さを削減する。
- まず macro_rules! 版を作り、必要なら proc-macro に拡張する。
- Item/Part の型パラメータはクレートが公開する Kind 型を使う。
## 仕様の前提
- Item は OR の item (message, function_call, reasoning など) に対応する。
- Part は OR の content part (output_text, reasoning_text など) に対応する。
- Item は必ず start/stop を持つ。Part は Item 内で複数発生し得る。
- Item/Part の型指定は `Item<Message>` / `Part<ReasoningText>` のように書く。
## 設計ステップ
### 1. 内部イベントモデルの整理
- Event を Item/Part/Meta の3層に整理する。
- ItemEvent / PartEvent は型パラメータで区別する。
- 例: ItemEvent<Message>, PartEvent<Message, OutputText>
### 2. スコープの二段化
- Item ctx: Item 型ごとに1つ
- Part ctx: Part 型ごとに1つ
- Part のイベントでは常に item ctx と part ctx の両方を渡す。
### 3. Handler trait の再定義
- Item/Part を型で指定できる trait を導入する。
- 例:
- trait ItemHandler<I>
- trait PartHandler<I, P>
- PartHandler には ItemHandler の ItemCtx を必須で渡す。
- Part の ctx 型は `PartKind::Ctx` 方式 or enum 方式で切り替える。
### 4. Timeline との結合
- Timeline は ItemStart で ItemCtx を生成
- PartStart で PartCtx を生成
- Delta/Stop は対応 ctx に流す
- ItemStop で ItemCtx を破棄
### 5. DSL (macro_rules!) の導入
- まず宣言的 DSL を提供する。
- 例:
- handler! { Item<Message> { type ItemCtx = ...; Part<OutputText> { type
PartCtx = ...; } } }
- DSL は ItemHandler / PartHandler 実装を生成する。
- Item/Part の Kind 型はクレートが公開する型を参照する。
### 6. 拡張ポイント
- 追加 Part (output_image など) を DSL に追加しやすい形にする。
- 必要なら proc-macro に移行して構文自由度を上げる。
## 実装順序
1. Event/Item/Part の型定義の整理
2. Item/Part ctx を持つ Timeline 実装
3. Handler trait の定義・既存コードの移行
4. macro_rules! DSL の実装
5. 既存ユースケースの移植
## TODO
- Item と Part の型対応表を整理する
- OR と既存 llm_client の差分を再確認する
- Tool args の delta を OR 拡張として扱うか検討する
- macro_rules! で表現可能な DSL の最小文法を確定する
+80
View File
@@ -0,0 +1,80 @@
# Open Responses mapping (llm_client -> Open Responses)
This document maps the current `llm_client` event model to Open Responses items
and streaming events. It focuses on output streaming; input items are noted
where they are the closest semantic match.
## Legend
- **OR item**: Open Responses item types used in `response.output`.
- **OR event**: Open Responses streaming events (`response.*`).
- **Note**: Gaps or required adaptation decisions.
## Response lifecycle / meta events
| llm_client | Open Responses | Note |
| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `StatusEvent::Started` | `response.created`, `response.queued`, `response.in_progress` | OR has finer-grained lifecycle states; pick a subset or map Started -> `response.in_progress`. |
| `StatusEvent::Completed` | `response.completed` | |
| `StatusEvent::Failed` | `response.failed` | |
| `StatusEvent::Cancelled` | (no direct event) | Could map to `response.incomplete` or `response.failed` depending on semantics. |
| `UsageEvent` | `response.completed` payload usage | OR reports usage on the response object, not as a dedicated streaming event. |
| `ErrorEvent` | `error` event | OR has a dedicated error streaming event. |
| `PingEvent` | (no direct event) | OR does not define a heartbeat event. |
## Output block lifecycle
### Text block
| llm_client | Open Responses | Note |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `BlockStart { block_type: Text, metadata: Text }` | `response.output_item.added` with item type `message` (assistant) | OR output items are message/function_call/reasoning. This creates the message item. |
| `BlockDelta { delta: Text(..) }` | `response.output_text.delta` | Text deltas map 1:1 to output text deltas. |
| `BlockStop { block_type: Text }` | `response.output_text.done` + `response.content_part.done` + `response.output_item.done` | OR emits separate done events for content parts and items. |
### Tool use (function call)
| llm_client | Open Responses | Note |
| -------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `BlockStart { block_type: ToolUse, metadata: ToolUse { id, name } }` | `response.output_item.added` with item type `function_call` | OR uses `call_id` + `name` + `arguments` string. Map `id` -> `call_id`. |
| `BlockDelta { delta: InputJson(..) }` | `response.function_call_arguments.delta` | OR spec does not explicitly require argument deltas; treat as OpenAI-compatible extension if adopted. |
| `BlockStop { block_type: ToolUse }` | `response.function_call_arguments.done` + `response.output_item.done` | Item status can be set to `completed` or `incomplete`. |
### Tool result (function call output)
| llm_client | Open Responses | Note |
| ----------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- |
| `BlockStart { block_type: ToolResult, metadata: ToolResult { tool_use_id } }` | **Input item** `function_call_output` | OR treats tool results as input items, not output items. This is a request-side mapping. |
| `BlockDelta` | (no direct output event) | OR does not stream tool output deltas as response events. |
| `BlockStop` | (no direct output event) | Tool output lives on the next request as an input item. |
### Thinking / reasoning
| llm_client | Open Responses | Note |
| --------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BlockStart { block_type: Thinking, metadata: Thinking }` | `response.output_item.added` with item type `reasoning` | OR models reasoning as a separate item type. |
| `BlockDelta { delta: Thinking(..) }` | `response.reasoning.delta` | OR has dedicated reasoning delta events. |
| `BlockStop { block_type: Thinking }` | `response.reasoning.done` | OR separates reasoning summary events (`response.reasoning_summary_*`) from reasoning deltas. Decide whether Thinking maps to full reasoning or summary only. |
## Stop reasons
| llm_client `StopReason` | Open Responses | Note |
| ----------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------- |
| `EndTurn` | `response.completed` + item status `completed` | |
| `MaxTokens` | `response.incomplete` + item status `incomplete` | |
| `StopSequence` | `response.completed` | |
| `ToolUse` | `response.completed` for message item, followed by `function_call` output item | OR models tool call as a separate output item. |
## Gaps / open decisions
- `PingEvent` has no OR equivalent. If needed, keep as internal only.
- `Cancelled` status needs a policy: map to `response.incomplete` or
`response.failed`.
- OR has `response.refusal.delta` / `response.refusal.done`. `llm_client` has no
refusal delta type; consider adding a new block or delta variant if needed.
- OR splits _item_ and _content part_ lifecycles. `llm_client` currently has a
single block lifecycle, so mapping should decide whether to synthesize
`content_part.*` events or ignore them.
- The OR specification does not state how `function_call.arguments` stream
deltas; `response.function_call_arguments.*` should be treated as a compatible
extension if required.