SherlockLiu Logo SherlockLiu
Back to all posts
Engineering

The LLM Layer: One Message Format, Every Surface (Part 10)

SL
Aug 14, 2026 7 min read
The LLM Layer: One Message Format, Every Surface (Part 10)

Series: Inside the DeepSeek Harness — Part 10 of 16


The LLM layer is where Part 6’s invariant — model-visible means logged — gets its hardest test. Every message the UI renders, every message the durable log stores, and every message that actually crosses the wire to a model provider needs to be the same object, or the whole “the log is the source of truth” claim quietly stops being true. This post covers how dsh keeps that promise, and ends on the single cleverest mechanism in this series so far: a retry counter that isn’t stored anywhere except the log itself.


One Message type, three consumers

packages/llm/llm/src/message.ts defines exactly one Message shape — {id, role, content: ContentBlock[], source} — immutable and frozen, and it’s the type shared by delivery (what the UI shows), durable history (what the log stores), and model requests (what actually gets sent). ContentBlockMap is merge-extensible the same way SessionEventMap was in Part 6 (text | reasoning | image | tool-call | tool-result); a new modality only truly belongs once the adapter, the UI, compaction, and durable replay all agree on how to handle it.

MessageSource has two independent axes kind — producer identity user | plugin | model | tool who actually produced this content form — presentation form instructions | catalog | snapshot | notice | relay | recall semantic, not visual — no colors

Figure: who produced a message, and how it's meant to be understood, are deliberately independent — a consumer can key off either without needing the other.


The chunk protocol: one fold, everywhere

Adapters translate provider-specific streaming formats into one canonical StreamChunk protocol — a closed discriminated union (block-start | text-delta | reasoning-delta | tool-call-delta | block-end | usage | finish) with a switch that ends in assertNever, so adding a new chunk kind without updating every consumer is a compile error, not a runtime surprise. One shared BlockAssembler (packages/llm/llm/src/assembler.ts) folds those chunks into a frozen AssistantMessage — every adapter feeds the same assembler, rather than each provider integration writing its own accumulation logic.

block-start, deltas, block-end, usage, finish BlockAssembler the ONE shared fold frozen AssistantMessage logged with usage + sourceEventSeqs On max-tokens: partial tool calls that can't be safely run get dropped here, not downstream.

Figure: one assembler for every provider — a provider bug can produce a wrong chunk sequence, but it can't produce a differently-shaped assembled message.

Two failure paths are sanctioned, and only two: a thrown error (transport/protocol failure) or a finish { kind: 'error' | 'aborted', failure } chunk (an in-band provider error). Both normalize to the same LlmFailure shape before reaching a consumer — LlmRuntime.stream() never lets a provider’s raw throw leak through unnormalized. One specific rule is worth calling out because it came from a real production fix, not a first-pass design: an empty completion is treated as a retryable error, not a silent success. Both shipping adapters map a contentless stop finish to EMPTY_RESPONSE, retried by default — documented directly against a real, dated incident note (2026-07-24-empty-model-response-is-retryable.md). Before that fix, a model that returned nothing would have looked, to the rest of the system, exactly like a model that had nothing more to say.


Retry: the state isn’t in memory — it’s derived from the log

This is the mechanism worth reading this whole post for. The retry policy’s defaults are ordinary — maxRetries: 2, initialDelayMs: 500, maxDelayMs: 10_000, jitterRatio: 0.1, retryable on EMPTY_RESPONSE | RATE_LIMIT | SERVER | TIMEOUT | TRANSPORT (packages/llm/llm/src/retry-policy.ts:14-24). What’s not ordinary is how the retry count itself is tracked.

There’s no counter variable held in memory across retries. Instead, on every agent/request-error, the retry handler scans the durable session log — agent.session.events.findLast(...) — for a prior llm/retry event matching the current turn, step, provider, and a stable JSON fingerprint of the resolved policy called policyKey. That scan is the count. If it finds two prior llm/retry events for this exact turn/step/provider/policy combination, this is attempt three, and the max-retries check applies against that derived number — nowhere else.

the durable log llm/retry #1 llm/retry #2 CRASH — process dies process restarts, replays the log findLast(turn, step, provider, policyKey) correctly resumes at attempt 3

Figure: retry state survives a crash for free, with zero special persistence code — it's the exact same "state lives in the log" pattern from Part 6, applied one level down to internal policy state.

Change the retry configuration — a different maxRetries, a different backoff, a reordered list of retryable codes — and policyKey changes with it, which means the retry count silently resets, because as far as the log is concerned it’s now a different policy being tracked. That’s not a bug; it’s the natural consequence of deriving state from a fingerprinted key instead of storing it separately from what it describes. The llm/retry event itself is durably appended before the cancellable backoff delay even starts — so a crash mid-wait still leaves an accurate record behind, ready to be replayed correctly on restart.

There’s also an always mode beyond the normal bounded retry — unbounded retry until success, cancellation, or disposal — implemented as a downstream-first fallback: it awaits whatever the normal retry decision would have been, and only synthesizes its own infinite retry if the normal path declined. Backoff itself is min(initialDelayMs * 2^min(retry-1, 1024), maxDelayMs), jittered into [1-jitterRatio, 1+jitterRatio] — the 1024 cap on the exponent exists purely to avoid numeric overflow and is otherwise irrelevant, since maxDelayMs already caps the real value long before the exponent gets anywhere near that size.


Multi-provider details: one adapter, many routes — safely

llm-deepseek is the simple case: one adapter, one route, direct fetch()-based SSE. llm-pi-ai is the more interesting one — a single adapter instance can own many provider routes at once, built from a profiles() map, and it ships in a deliberately dormant posture: zero routes registered until settings actually supply provider profiles, then it calls .replace(routes) for an atomic swap. Provider-level baseUrl on pi-ai is display metadata only — actual routing happens through the library’s own internal resolution, not the harness-visible URL. Each resolution produces one immutable snapshot, which is exactly what prevents a hot-reload from ever combining one adapter generation’s capability result with another’s mid-request — a request either sees the whole old world or the whole new one, never a mix.


Coming up in this series

(All 16 parts are live today — no daily drip.)

Part Title What it covers
1 DeepSeek Harness: Inside the Open-Source Claude Code Rival The launch, the comparison, no privileged core, the four Cordis primitives
2 Composing an App From YAML, Not Code Profiles, bundles, five patch layers, boot, live HMR, --dump-config
3 Scope: Why a Live Agent Is the Key of Its Own Registration Shadowing, restriction, lineage vs. scope
4 Tool Execution in DeepSeek Harness: Guards and Approval Pre-execute, monotonic guards, post-execute, approval
16 DeepSeek Harness: Agent Presets as Data, Not Code Agent presets, PTC/Code Mode, the plugin ecosystem, compared to Claude Code’s agent types
5 Capability Seams: Making Bash Swappable for Sandboxed Bash The 3-role pattern end-to-end, across ~85 real seams
6 The Session Log: DeepSeek Harness’s Enforced Invariant The real invariant-checking code, surface projection
7 Persistence and Compaction: Crash-Safe by Construction JSONL/SQLite, torn-tail repair, the compaction lock bracket
8 Waterfalls: The One Event Pattern That Runs Everything Five dispatch modes, durable vs. live events, retry
9 The Agent Loop: Turns, Steps, and a Real Cancellation Bug The phase state machine, the inbox, a dated bug fix
10 The LLM Layer: One Message Format, Every Surface (this post) Message vocabulary, streaming, retry-via-log-replay
11 Subagents and Workflows: Composing Agents From Agents Provider kinds, continuable children, Ralph rounds
12 Defense in Depth: Sandboxing and Four Real Incidents bwrap/Landlock/Seatbelt/ACL, real production postmortems
13 Three Surfaces, One Spine: Web, Typert RPC, and SDK/ACP Typert codegen, the four-quadrant RPC envelope
14 Engineering Rigor: DeepSeek Harness’s Verification Gate The 100% coverage gate, real engineering-culture quotes
15 What a Second Production Harness Teaches dsh vs. the Agent Harness series, side by side

Part 16 shipped after this series’ initial 15 posts, once Agent Presets and Code Mode landed — it reads best right after Part 4, which is why its row sits there instead of at the end.

Next: Part 11 zooms out to how dsh composes agents from agents — subagent provider kinds, the difference between a one-shot delegation and a genuinely continuable background conversation, and a workflow engine that never lets a script hang forever.


References

Comments