summaryrefslogtreecommitdiff
path: root/docs/phase-2.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/phase-2.md')
-rw-r--r--docs/phase-2.md157
1 files changed, 0 insertions, 157 deletions
diff --git a/docs/phase-2.md b/docs/phase-2.md
deleted file mode 100644
index 4cb7ed9..0000000
--- a/docs/phase-2.md
+++ /dev/null
@@ -1,157 +0,0 @@
-# Phase 2: Anthropic Provider
-
-**Status: complete.** Anthropic Messages API streams end-to-end alongside the phase-1 OpenAI provider; the Receiver callback sequence is identical across both; system prompts extract to the top-level field; thinking blocks round-trip via captured `signature` deltas. Refinements that diverged from the original plan: file/type names use the explicit dialect (`provider_anthropic_messages`, `AnthropicMessagesConfig`) to leave room for `openai_responses` and future Anthropic shapes; `Config` is a tagged union keyed by `APIStyle` rather than separate types at call sites; concrete provider types are internal — callers construct via `provider.Provider.init(allocator, io, config)`. Wire-level gotchas worth remembering: both providers send `accept-encoding: identity` because gzip buffers small SSE frames and defeats streaming; the read loop uses `readVec` rather than `readSliceShort` because the latter fills its buffer before returning, which also defeats streaming. Tool use / tool result blocks remain stubbed for phase 3+.
-
-## Goal
-
-Add Anthropic as a second provider, validating that the Provider abstraction and internal conversation model are genuinely provider-agnostic — not just OpenAI in disguise.
-
-## Deliverable
-
-A working `provider_anthropic.zig` that can hold a streaming conversation via Anthropic's API. At the end of this phase, you can:
-
-- Switch between OpenAI and Anthropic providers with no changes to the agent loop or conversation model.
-- Stream responses from either provider and see the same callback sequence (onMessageStart, onBlockStart, onContentDelta, onBlockComplete, onMessageComplete).
-- Observe thinking and text content streaming correctly from Anthropic models.
-
-## What is usable at the end
-
-| Capability | How to exercise it |
-|---|---|
-| Create an Anthropic provider | `AnthropicProvider.init(allocator, config)` |
-| Run a chat via Anthropic | Same `agent.runStep()` call, different provider |
-| Stream Anthropic responses | Same Receiver interface, same callback sequence |
-| System prompts with Anthropic | System messages extracted and sent as top-level system field |
-
-## What is explicitly out of scope
-
-- Tools and tool-use (phase 3+)
-- Extensions (phase 3+)
-- Conversation serialization / disk persistence (phase 4)
-- Server/proxy mode (future)
-- Google API provider (future)
-
----
-
-## Receiver Interface
-
-The Receiver interface with the full 5-callback lifecycle is defined in phase 1. Both providers must produce the same callback sequence:
-
-- onMessageStart → onBlockStart → onContentDelta(s) → onBlockComplete → ... → onMessageComplete
-
-See `phase-1.md` for the full definition and contract.
-
-### How OpenAI synthesizes the callbacks
-
-Defined in phase 1. OpenAI has no explicit block boundaries; the provider infers them via a state machine that tracks the active block type and emits start/complete callbacks on transitions.
-
-### How Anthropic maps to the callbacks
-
-Anthropic's structured events map directly:
-
-| Anthropic event | Callback |
-|---|---|
-| `message_start` | onMessageStart(.assistant) |
-| `content_block_start` | onBlockStart(type, index, meta if ToolUse) |
-| `content_block_delta` | onContentDelta(index, delta bytes) |
-| `content_block_stop` | onBlockComplete(index, assembled block) |
-| `message_delta` + `message_stop` | onMessageComplete(assembled message) |
-
-No inference needed — Anthropic gives us explicit boundaries.
-
----
-
-## Anthropic Request Serialization
-
-### Wire format differences from OpenAI
-
-| Aspect | OpenAI | Anthropic |
-|---|---|---|
-| System prompt | Messages with `role: "system"` | Top-level `system` field (string) |
-| Content shape | String or array of parts | Always array of content blocks |
-| Tool results | Separate `role: "tool"` messages | Content blocks on `role: "user"` messages |
-| Auth | `Authorization: Bearer <key>` | `x-api-key: <key>` + `anthropic-version` header |
-| Streaming | `stream: true` in request body | `stream: true` in request body |
-
-### Serialization rules
-
-**System messages**: Extract all `role=.system` messages from the conversation. Concatenate their Text block contents into a single string. Set as the top-level `system` field. Do not include them in the messages array.
-
-**User messages**: Emit as `role: "user"`. Content blocks become Anthropic content block format:
-- Text → `{ "type": "text", "text": "..." }`
-- ToolResult → `{ "type": "tool_result", "tool_use_id": "...", "content": "..." }` (phase 3+)
-
-**Assistant messages**: Emit as `role: "assistant"`. Content blocks:
-- Text → `{ "type": "text", "text": "..." }`
-- Thinking → `{ "type": "thinking", "thinking": "..." }`
-- ToolUse → `{ "type": "tool_use", "id": "...", "name": "...", "input": {...} }` (phase 3+)
-
-Note: Anthropic expects ToolUse's `input` as a parsed JSON object, not a string. Since we store `input` as raw bytes in a TextualBlock, we will need to parse it into a `std.json.Value` during Anthropic serialization. This is the one place where libpanto does parse tool input JSON — it's a serialization requirement, not an interpretation of the tool schema. The round-trip guarantee is: the bytes we stored serialize back to equivalent JSON when sent to Anthropic.
-
----
-
-## Anthropic Streaming Event Parser
-
-Each SSE event is a complete JSON object. The event type is in a top-level `type` field.
-
-### Event types and handling
-
-| Event type | What we extract | Action |
-|---|---|---|
-| `message_start` | `message.role`, `message.id`, `message.model` | Emit onMessageStart; begin assembling Message |
-| `content_block_start` | `content_block.type`, `content_block.index`, `content_block.id`, `content_block.name` | Emit onBlockStart; create new TextualBlock or ToolUseBlock |
-| `content_block_delta` | `delta.type` (text_delta, thinking_delta, input_json_delta), `delta.text` or `delta.thinking` or `delta.partial_json` | Append to current block's buffer; emit onContentDelta |
-| `content_block_stop` | `index` | Emit onBlockComplete with assembled block |
-| `message_delta` | `delta.stop_reason`, `usage` | Track stop reason for onMessageComplete |
-| `message_stop` | (none) | Emit onMessageComplete with fully assembled Message |
-
-The parser is a separate concern from the SSE line parser (`sse.zig`). The SSE parser reassembles byte chunks into complete `data: {...}` events. The Anthropic event parser interprets the JSON of each event. They compose: `HTTP read → SSE parser → event strings → Anthropic event parser → callbacks`.
-
----
-
-## Module Changes
-
-### New files
-
-```
-src/provider_anthropic.zig // Anthropic provider implementation
-```
-
-### Modified files
-
-- `provider.zig` — ReceiverVTable expanded to the 5-callback lifecycle
-- `provider_openai.zig` — No changes needed (block synthesis already built in phase 1)
-- `agent.zig` — Any changes needed for expanded Receiver (should be minimal since Receiver is an interface)
-
-### Config
-
-```
-AnthropicConfig = struct {
- api_key: []const u8,
- base_url: []const u8, // e.g. "https://api.anthropic.com"
- model: []const u8, // e.g. "claude-sonnet-4-20250514"
- api_version: []const u8, // e.g. "2023-06-01"
-};
-```
-
-Separate from `OpenAIConfig`. The CLI and any higher-level config can unify them; libpanto treats them as distinct.
-
----
-
-## Testing Strategy
-
-### Unit tests
-
-| What | How |
-|---|---|
-| Anthropic serialization | Create conversations with system/user/assistant messages, serialize to Anthropic JSON, verify structure and system prompt extraction |
-| Streaming event parser | Feed canned Anthropic SSE events (message_start, content_block_start, etc.) and verify correct callback sequence and assembled output |
-| Block boundary synthesis | Feed OpenAI-style deltas (reasoning → content transitions) and verify onBlockStart/onBlockComplete emitted correctly |
-| Receiver contract | Verify that both providers produce the same callback sequence for equivalent conversations |
-
-### Integration test (manual)
-
-- Run `panto` binary against Anthropic API with a real API key
-- Hold a multi-turn conversation with thinking model
-- Verify thinking and text stream correctly
-- Switch to OpenAI provider, verify same experience