diff --git a/docs/adr/0017-agent-session-is-provider-bound.md b/docs/adr/0017-agent-session-is-provider-bound.md index 37af679..331cf3a 100644 --- a/docs/adr/0017-agent-session-is-provider-bound.md +++ b/docs/adr/0017-agent-session-is-provider-bound.md @@ -1,50 +1,53 @@ -# ADR 0017: AgentSession Is Provider-Bound +# ADR 0017: Agent Runtime — Claude Code SDK ## Status -Accepted. +Accepted — supersedes the original ADR-0017 (provider-agnostic session). ## Context -ADR-0002 says a long-lived `AgentSession` can be reused by many runs, but never -addressed the provider/model dimension. The agent layer is provider-agnostic -(ADR-0001..0003 never required Claude specifically; the `@Claude` trigger name -is a product brand, not a provider commitment). In practice the Hub routes -model calls through an OpenAI-compatible client (e.g. OpenRouter), so a run may -target different models — and the motivation for multi-model is **role-based -routing**: drafting, reviewing, and triage suit different models. +The original ADR-0017 mandated a provider-agnostic agent layer: any +OpenAI-compatible model via OpenRouter, custom agent loop built on Vercel AI +SDK's `streamText`. The motivation was to avoid vendor lock-in. -That raises the question ADR-0002 left open: when a teacher switches model -mid-project, does the `AgentSession` carry live context across the switch, or -is it bound to a single provider/model? +In practice, the hand-rolled agent loop lacked capabilities that a production +agent needs: context compaction (long conversations), tool approval flows, +partial-message streaming, and a battle-tested multi-turn loop. Building these +ourselves meant re-implementing what the Claude Code SDK already provides. + +The Feishu integration also proved costly to hand-roll — card schema quirks, +patch limitations, file support — each API quirk cost a round-trip. The +Claude-to-IM library (2.7K stars) has already solved these, but its +LLMProvider interface expects Claude Code SDK's SSE event format. Using it with +a generic provider would require a conversion layer. ## Decision -An `AgentSession` is **provider/model-bound**. A model or provider switch -starts a new `AgentSession`. The `AgentSession` does not carry live context -across a switch. +Adopt `@anthropic-ai/claude-agent-sdk` as the agent runtime. The SDK's `query()` +function owns the agent loop (tool dispatch, compaction, streaming). Built-in +tools (Read, Write, Bash, Glob, Grep) cover the entire tool surface — no custom +tools needed. `cph check` / `cph build` run via Bash. -Cross-session continuity is not the session's job — it is carried by ADR-0003's -project memory and anchors, which the Hub reads on demand when seeding a new -run. This keeps B consistent with ADR-0003 ("the Hub avoids becoming a full -chat history store"): session continuity and history continuity are the same -mechanism, both via memory/anchors, never via a live cross-provider session. - -Per-run model selection (the run carries its model) is the switching point. +This means: +- The agent layer is **Anthropic-specific**, not provider-agnostic. Claude Code + SDK speaks the Anthropic Messages API and cannot be pointed at OpenRouter + (which is OpenAI-compatible). +- An Anthropic API key (or OAuth via Claude Pro/Max) is required. +- Model selection is within the Anthropic model family (Sonnet, Haiku, Opus), + not across vendors. ## Consequences -- Switching model mid-project = new session; the new run seeds from project - memory/anchors (ADR-0003), not the prior session's live context. -- The agent layer is provider-agnostic: model calls go through an - OpenAI-compatible client, and the tool-calling loop is delegated to the - Vercel AI SDK (`generateText` + `tool`). The provider seam is the SDK's - `LanguageModel` interface, not a hand-rolled loop; swapping provider is - swapping the `createOpenAICompatible` factory, not rewriting the loop. No - hard dependency on a single-vendor agent SDK. -- Role-based model routing (different run kinds default to different models) is - a product/admin configuration concern, not a spec invariant. -- "AgentSession reused by many runs" (ADR-0002) holds *within* one - provider/model; it does not span a switch. -- The `@Claude` trigger name remains the product mention brand regardless of the - backing model. +- We lose the ability to route to GLM or other non-Anthropic models via + OpenRouter. This is the accepted cost of getting a mature agent loop. +- The Vercel AI SDK dependency is dropped. +- Custom tools (read_file, write_file, cph_check, cph_build) are replaced by + the SDK's built-in Read/Write/Bash/Glob/Grep. +- The SSE event format matches Claude-to-IM's expected protocol natively — + adopting Claude-to-IM for Feishu integration is a direct fit, no conversion. +- Per-run model selection still works (role→model mapping), but models are + Anthropic aliases, not OpenRouter routing IDs. +- If provider-agnosticism becomes a hard requirement again, the migration path + is: replace `query()` with a Vercel AI SDK `streamText` call + an SSE format + converter (~100 lines). The rest of the architecture (trigger, lock, + permission, Feishu client) is provider-agnostic already.