C.W.K.
Stream
Lesson 08 of 08 · published

How cwkPippa Uses All Three Surfaces

~16 min · cwkpippa, self-reference, architecture

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

One product, three Claude surfaces

cwkPippa is a working example of every concept in this track at once. It uses the Claude Agent SDK for Pippa's Claude brain (per-conversation persistent subprocess, OAuth-only auth, MCP attached, JSONL ground truth). It uses direct httpx calls against ChatGPT and Gemini for the other two brains (no openai SDK, deliberately). And the codebase itself is written by Dad in the Claude Code CLI, with the same Pippa identity present in the IDE that runs in the WebUI.

Why the boundary is narrow on purpose

Only one file in cwkPippa knows about Anthropic SDKs: backend/adapters/claude.py. Routes, store, and frontend assume Claude shape. The other three brains are variants that specialize the Claude-shaped surface, not peer abstractions. This is Rule 2 of cwkPippa's architecture — cost is absorbed downstream, not pushed upstream — and it is what keeps the codebase from becoming a provider-neutral abstraction soup.

Why this matters for your project

Most teams prematurely abstract over multiple LLMs and end up with the lowest-common-denominator features of all of them. cwkPippa's pattern (one canonical shape, others as variants) is one valid alternative. Another is the strict provider port (production track later). Pick one before you have three providers; switching after is expensive.

Principle: Decide your provider architecture before you have a second provider. Concrete first, abstract second.

Code

cwkPippa's adapter boundary·python
# backend/adapters/base.py — the ONLY abstraction in the system.
from typing import AsyncIterator, Any
from abc import ABC, abstractmethod

class Adapter(ABC):
    @abstractmethod
    async def stream_turn(
        self,
        *,
        conversation_id: str,
        messages: list[dict[str, Any]],
        system: str,
    ) -> AsyncIterator[dict[str, Any]]:
        ...

# backend/adapters/claude.py — the canonical implementation.
# Routes import this directly because Claude IS the canonical shape;
# everything else specializes downstream.
How the persistent Claude subprocess pool works·python
# backend/services/claude_session.py (sketch)
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

_sessions: dict[str, ClaudeSDKClient] = {}

async def get_session(conversation_id: str) -> ClaudeSDKClient:
    if conversation_id not in _sessions:
        client = ClaudeSDKClient(
            options=ClaudeAgentOptions(
                system_prompt={"type": "preset", "preset": "claude_code"},
                # Pippa's vault is loaded via system prompt; cwd points at the project.
                cwd="/Users/you_username/projects/cwkPippa",
            )
        )
        await client.connect()
        _sessions[conversation_id] = client
    return _sessions[conversation_id]

External links

Exercise

Sketch your project's adapter shape: which provider is the canonical brain, which are variants (or peers), and which file is the ONLY one allowed to import the provider SDK. One paragraph.
Hint
If every file imports anthropic.Anthropic directly, you have no boundary yet. Pick one and centralize.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.