The LLM service exposed three call surfaces (stream/streamBlocks/generate) but the only production consumer — the agent loop — uses stream() exclusively, feeding raw chunks through its own BlockAssembler for replay fidelity. Drop the speculative convenience surfaces and the registry-change event that no listener consumed, leaving stream() as the single model-call contract for both production and tests. - Remove LlmService.streamBlocks() and generate(), the llm/generate waterfall, and GenerateResult. - Remove the llm/adapter-change event (declaration + emits) and the listener-throw rollback ordering that existed only to protect it; keep the HMR rollback disposer. - Remove BlockAssembler.flushReady()/flushRemaining()/result() and the flushed cursor — the streaming-flush slice existed only for streamBlocks(). - Adapter tests drive a stream()+BlockAssembler helper (tests/assemble.ts) instead of generate(), exercising the same path production uses. - Land the AGENTS.md "RFCs are proposals, not golden truth" principle and move both RFCs proposed -> implemented. Implements: - docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md - docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
@deepseek-ai/dsh-llm-pi-ai
DeepSeek adapter for the harness LLM seam backed by @earendil-works/pi-ai (the LLM library behind the pi agent).
Why a second adapter exists
@deepseek-ai/dsh-llm-deepseek already talks to the same endpoint. This package is its design-verification twin: same models, same wire protocol, completely different internals — a unified LLM library with its own event vocabulary versus hand-rolled fetch/SSE. Anything the harness StreamChunk protocol cannot express for BOTH implementations is a core-vocabulary bug. The differences it exercised on purpose:
- pi-ai hands tool-call
argumentsaround as parsed objects; the harness keeps raw JSON strings. The adapter patches replay payloads back to the original raw strings before sending them, and re-stringifies parsed output tool calls atblock-end. - pi-ai reports failures as in-stream error events (it never throws mid-stream); these map to
finish {kind:'error'|'aborted'}chunks — the protocol's other sanctioned error path besides throwing (which llm-deepseek uses). - pi-ai folds reasoning tokens into
usage.output; there is no separate reasoning count to map. - pi-ai's options omit some DeepSeek/OpenAI-compatible details; the adapter uses its
onPayloadhook to preserve the harness contract (stop, per-toolstrict, omitted reasoning effort, raw replayed tool arguments).
Config
Same shape as llm-deepseek (one-line swap in cordis.yml), with pi-ai's thinking-level vocabulary:
- id: llm
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
models: [deepseek-v4-flash, deepseek-v4-pro]
reasoning: high # off | high | xhigh (xhigh → wire 'max')
Dependency weight
pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time dependencies. They are lazy-loaded — only the openai SDK actually loads for this adapter — but they do land in node_modules. Accepted for a package whose purpose is design verification.
Limitations
Same MVP contract as llm-deepseek: prefill throws UNSUPPORTED, images are not representable, tool_choice is not mapped.
Testing
Unit suites run against a local node:http mock SSE server (pi-ai's openai SDK happily talks to any base URL). Real-API coverage in tests/adapter.e2e.ts (pnpm run test:e2e, key-gated): V4 Flash + V4 Pro across all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip, and a cross-adapter structural-equivalence check against llm-deepseek.