DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Core Runtime·Chapter 08

LLM Adapters and Conversation Protocol Translation

How internal messages project into DeepSeek, Pi AI, and replay adapters

VerifiedUpstream 47f943859bScope: Analyze internal messages and streams, adapter registration and selection, tool calls, reasoning, usage, and error normalization.

Conclusion: an adapter translates one call; the Session Log owns the conversation

DeepSeek Harness never lets a provider SDK become the owner of conversation state. It first constructs an immutable, sourced Message vocabulary and a closed StreamChunk protocol. An adapter only projects one complete request onto a wire protocol and translates the response back. Persistence, retries, replay, and tool continuation all remain outside the adapter.

The boundary does more than enable multiple models. It confines provider differences to four surfaces—capability resolution, wire serialization, stream translation, and error classification—while the Agent Loop applies the same Turn/Step, request-header, and SessionEvent semantics to every dispatch.

1. One model call crosses six commit boundaries

1

Derive history from the log

The Loop reads the current request header and Session.deriveMessages(); it never asks an SDK for conversation history.

2

Prepare the exact call

prepareCall() resolves one route, exact-model capabilities, adapter defaults, context capacity, and retry policy.

3

Commit request facts

The canonical header and request context enter the Session before the final request is deep-frozen.

4

Cross a durability barrier

llm/stream middleware flushes the complete request prefix before constructing the downstream adapter stream.

5

Record the raw stream

Every chunk is appended as assistant/chunk before it enters the canonical assembler.

6

Commit the semantic message

Only a successful finish produces one assistant/message; request-error policy owns failures.

Production control flow

The request, raw chunks, and final message inside one Step all carry the same Turn and Step. Providers never receive the Session object and never decide when another Step begins.LOOP-STEP-STATELOOP-REQUEST-BUILD

Pre-effect barrier

Model I/O is an explicit semantic side-effect boundary. If request facts cannot be made durable, the checkpoint listener never invokes the adapter.LLM-CHECKPOINT-BARRIER

2. The internal protocol is deliberately narrower than any one provider

LayerCore representationCritical constraintWhy it matters
Message{id, role, content, source}Cloned and deep-frozen at creation; stable identity crosses delivery, log, and requestThere is no second SDK-owned history authority LLM-MESSAGE-VALUE
Contenttext / reasoning / image / tool-call / tool-resultTool arguments retain the raw JSON string produced by the modelParsing belongs at the tool boundary; the adapter does not rewrite intent LLM-CONTENT-PROTOCOL
Provenanceuser / plugin / model / toolAssistant messages name provider and model and may carry replay stateCross-provider history cannot impersonate a native response LLM-MESSAGE-IDENTITY
Streamblock-start / delta / block-end / usage / finishIndexes permit interleaving; usage precedes terminal finish; nothing follows finishUI, log, assembler, and adapters share one stream contract LLM-STREAM-PROTOCOL
Requestconfig + system + tools + messagesProvider and model route exactly; sessionId and purpose are model-hidden metadataConversation semantics stay separate from transport metadata

The vocabulary is declaration-merge extensible, but built-in adapters do not automatically behave symmetrically for an unknown block. A new block type needs adapter, UI, compaction, and replay support; extending a TypeScript union alone does not create an end-to-end capability.

3. A provider route is a unique mapping, not a fallback list

Registry contract

One provider string maps to at most one adapter registration. Empty routes, duplicates, or a metadata ID mismatch fail synchronously, and a multi-route registration is all-or-nothing. Effect teardown removes exactly the routes still owned by that registration, while replace() swaps old and new route sets in one synchronous section.LLM-ADAPTER-REGISTRY

Prepared call

prepareCall() captures the registration, resolved config, retry policy, model context, and adapter-default markers. Its frozen handle can dispatch once and rejects caller-side config mutation as INVALID_PREPARED_CALL.LLM-PREPARED-CALL

Hot-replacement test

When a route is replaced after preparation, the old prepared call remains served by the old adapter and old retry policy. A fresh registry lookup sees the new registration.LLM-PREPARED-PIN-TEST

4. BlockAssembler is the sole semantic fold for successful responses

Assembly rules

The assembler retains insertion order by block index, tolerates adapters with delta-only protocols, and allows reasoning, text, and multiple tool calls to interleave. The first block-end becomes authoritative; later deltas or another close for that index are ignored.LLM-BLOCK-ASSEMBLER

  • Without an explicit finish, the accessor defaults to stop; production adapters should enforce their own terminal protocol.
  • Open text, reasoning, and tool-call blocks can be assembled from deltas; an unknown open block type fails.
  • On max-tokens, every tool-call block is filtered so truncated JSON arguments cannot execute.
  • The last usage chunk wins; replay state comes from the terminal finish.
  • Raw chunks have already entered the Session Log, so the semantic message is not the only observable layer.
Assessment

This is a consequential “one folding algorithm” choice: adapters emit facts instead of implementing their own message concatenation. The cost is deliberate tolerance for some malformed-stream branches; the assembler preserves agreement with an already closed and displayed block instead of failing on every protocol violation.

5. The direct DeepSeek adapter: narrow protocol, broad model passthrough

SurfaceActual behaviorBoundary or consequence
RouteFixed deepseek-officialThe advisory catalog starts with V4 Flash and Pro, but unknown model IDs pass through
CapabilityDefault 1,000,000 context, 256,000 output, text onlyThe catalog is discovery metadata, not an allowlist LLM-DEEPSEEK-MODELS
Thinkingoff/high/max; normally defaults high; a disabled deployment offers only offsession-title always disables thinking
ConfigurationEndpoint, credential, and last-good settings resolve for each requestA bad live settings snapshot never replaces the last serviceable one LLM-DEEPSEEK-CONFIG
TransportNative fetch POST to /chat/completions plus SSESends attribution, anonymous user ID, session ID, and a compaction-specific header LLM-DEEPSEEK-TRANSPORT
Wire projection

System becomes the first wire message, user text is joined, and internal user-role tool results expand into standalone role: tool messages. Assistant text and tool calls preserve order, but reasoning_content is replayed only on tool-call turns. Plain-turn reasoning remains in the durable internal message but does not enter later DeepSeek wire history. Images are rejected before any text-flattening path.LLM-DEEPSEEK-SERIALIZE

SSE translation

The translator waits for the literal [DONE] before closing blocks and emitting final usage and finish. Malformed JSON becomes MALFORMED_RESPONSE, missing DONE becomes STREAM_CLOSED, and a normal stop with no content becomes EMPTY_RESPONSE. Because DeepSeek prompt totals include cache hits, the adapter subtracts hits to form disjoint input buckets.LLM-DEEPSEEK-TRANSLATE

6. The Pi AI adapter: broad provider surface, strict exact-model surface

Two provider constructions

For an installed catalog route without a wire-API override, the Harness reuses the original pi-ai Provider and retains its native API implementation and ambient authentication. A manual route or protocol override permits only openai-completions, openai-responses, and anthropic-messages. Bedrock, Vertex, Azure, and OAuth-only paths are not advertised when the current configuration shape cannot express their requirements.LLM-PI-PROVIDER

Dynamic profiles

The plugin starts with no routes. Each configured profile owns model catalogs and overrides, endpoint, credential reference, reasoning mappings, thinking budgets, cache retention, SSE/WebSocket transport, timeouts, headers, and retry policy.LLM-PI-CONFIG

Call behaviorPi AI pathDifference from direct DeepSeek
Model resolutionMust exist in the current profile; unknown models fail before I/OUnlisted DeepSeek IDs pass through
Stop sequencesExplicitly unsupportedDeepSeek writes stop to the wire
ImagesRequire an image-capable model and the durable attachment serviceThe DeepSeek path is always text-only
RetriesThe SDK receives maxRetries: 0Both paths leave visible attempts to Agent recovery
ReasoningEnumerated per exact model; invalid effort is rejected rather than clampedDeepSeek uses fixed off/high/max levels
Error detailAfter the upstream library flattens Error/cause, classification uses message regexesThe direct path retains HTTP status, Retry-After, and request ID
Per-stream snapshot

Before the first await, the Pi adapter captures the profile, model collection, and descriptor, then resolves credentials. A configuration update during the active request cannot alter that snapshot; the next call sees the new one.LLM-PI-STREAM

7. “Replay” names two adapter-level mechanisms that must stay separate

Provider-native replay metadata

Harness Message content remains authoritative for Pi AI text, reasoning, and tool calls. PiAiReplayState v1 persists only API, source provider/model, response identity, stop reason, and per-block signatures. Restoration validates kind and version, source identity, block count, block types, and metadata.LLM-PI-REPLAY

LlmRuntime retains this private state only when the historical provider and target provider are currently owned by the same adapter instance. Otherwise it strips the state and reconstructs an ordinary “foreign assistant.” One adapter never interprets another adapter’s private protocol data.

Keyless test replay

dsh-llm-replay is a test and snapshot adapter, not production session recovery. It derives one provider call at each assistant/chunk finish, so retries sharing a Turn and Step become multiple script entries. A thrown stream or a hang cannot be recovered from the successful event log alone and requires an explicit sidecar override.LLM-REPLAY-DERIVATION

Boundary conclusion

Session replay reconstructs the next request; adapter replay state restores provider-native metadata for a historical response; test replay reproduces a recorded chunk stream. They solve different problems.

8. A retry is another provider attempt inside the same Step

Actual Loop

The while (true) in step() encloses request construction, streaming, and the request-error waterfall. After a failure yields {kind:'retry'}, it directly continues without closing the Step or Turn. The next attempt derives messages and rebuilds the request again while reusing the Step’s prompt assembly.PROMPT-RETRY-BOUNDARYLLM-RETRY-SAME-STEP-TEST

Policy executor

Normal mode defaults to at most two retries for EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, and TRANSPORT. Local delay grows exponentially from 500ms to 10s with 10 percent symmetric jitter. Always mode retries every request failure without a count limit until success, cancellation, or plugin disposal.LLM-RETRY-POLICY

Durable scheduling

The plugin appends llm/retry with policy key, failure, retry number, and delay before a cancellable wait. It appends llm/retry-started only after the wait and immediately before returning the retry action. A provider Retry-After value is accepted directly only when it does not exceed the cap.LLM-RETRY-EXECUTOR

Wire-test contract

The real mock-HTTP tests require byte-equivalent request bodies for failed and successful attempts. Failed partial chunks remain in the raw log but outside the surface, and only one assistant message is committed. A clean EOF is STREAM_CLOSED, which the default policy does not retry. Dependencies are not installed here, so this study reviewed but did not run the tests.LLM-RETRY-WIRE-TEST

9. The upstream retry documentation has drifted from the implementation

Confirmed documentation/source conflict

The llm-retry README says each retry opens a newly numbered Turn, that the Loop closes the failed Turn before a retry Turn, and that Agent Turns are the only retry boundary. Current production source and two test suites prove that attempts share the same Turn and Step.LLM-RETRY-DOC-DRIFTLOOP-REQUEST-ERROR-TEST

This is not a cosmetic wording issue. It changes how events are interpreted, grouped in a UI, attributed for cost, segmented into replay scripts, and folded by any projection that treats (turn, step) as request identity. Analysis and integrations must follow the current Loop and tests rather than copying the README’s “new Turn” narrative.

10. Error normalization encloses only adapter-owned failures

Exact boundary

Provider selection, adapter stream() invocation, iterator construction, and next() failures all become one terminal finish {error|aborted, failure}. The serializable failure snapshot can retain a Harness-owned code, HTTP status, provider delay, and request ID.LLM-FAILURE-BOUNDARY

  • A failure thrown by llm/stream middleware remains thrown instead of impersonating a provider failure.
  • A downstream consumer failure after a yield remains thrown.
  • An adapter-iterator cleanup failure after early consumer close remains thrown.
  • An adapter may throw an arbitrary non-Error value; normalization produces UNKNOWN without letting hostile coercion escape.
  • An already-aborted signal or an ABORTED failure code produces an aborted terminal reason.
Assessment

The distinction preserves responsibility. Provider and transport failures can enter common retry policy; plugin, consumer, and cleanup bugs cannot be silently swallowed by automatic retry. Pi AI’s text-based classification after upstream Error flattening is a capability degradation, not a requirement of the core protocol.

11. Tokens, caching, and one high-confidence accounting gap

SourceMappingFidelity boundary
DeepSeekinput = prompt_tokens - cache_hit; output passes through; reasoning may be presentNo cache-write field; reasoning is an output subdimension and is not added twice
Pi AIMaps input, output, cacheRead, and cacheWrite; zero cache buckets are omittedpi-ai already folds reasoning into output and does not report it separately
Harness projectionAccumulates uncached input, output, cache read, and cache writeThe latest sample for the same Turn and Step replaces the prior sample
Original intent

A usage chunk arrives before the final message, and a successful message carries the same usage again. The projection therefore uses (turn, step) to identify early and final samples for “one call,” subtracting the former before adding the latter to avoid double counting.LLM-TOKEN-USAGE-PROJECTION

Source-derived defect

Retries make multiple provider attempts share one (turn, step), while the replay test explicitly recognizes those attempts as separate calls. If a failed attempt emitted usage, the successful attempt’s usage replaces it rather than accumulating it, so the cumulative projection understates actual provider consumption.LLM-MULTI-ATTEMPT-REPLAY-TESTLLM-RETRY-SAME-STEP-TEST

12. A prepared registration does not pin the adapter’s dynamic configuration generation

Source-derived race window

prepareCall() pins the adapter registration and resolved model config. A checkpoint may then asynchronously flush. When iteration truly starts, the DeepSeek adapter calls options() again and the Pi adapter calls current() again. If settings update between those moments, logged model defaults or context may come from generation A while endpoint, profile, or credential comes from generation B.LLM-PREPARED-CALLLLM-CHECKPOINT-BARRIERLLM-DEEPSEEK-TRANSPORTLLM-PI-STREAM

The existing Pi snapshot test covers an update after the adapter stream has started, during credential resolution. It proves stability after that capture, but it does not cover the interval between preparation and first iteration. The Loop reconstruction invariant also compares model, system, temperature, maxTokens, stop, tools, and messages—but not provider or reasoningEffort.LLM-REQUEST-INVARIANT-SCOPE

13. Capability matrix for the two production adapters

DimensionDirect DeepSeekPi AIArchitectural meaning
Provider breadthOne official routeMultiple profiles and catalog providersNarrow and faithful versus broad and constrained by an upstream abstraction
Wire controlOwns fetch and SSE completelyDelegates to pi-ai provider APIsThe direct path retains more error and header facts
Model admissionCatalog-external IDs pass throughExact catalog membershipForward compatibility versus configuration closure
ImagesRejectedCapability plus attachment storeDurable binary ownership is an additional dependency
ReasoningFixed off/high/maxMapped per modelProvider-neutral IDs still require adapter validation
History fidelityTool-turn passback; plain reasoning is not replayedVersioned signatures and replay metadataThe same internal Message can have different wire fidelity
UsageCache read plus optional reasoningCache read/write with reasoning foldedCommon buckets do not imply equally rich source data
RetryEach adapter performs one attempt; Agent recovery owns visible retriesOne event, cost, and cancellation boundary

14. Strengths, costs, and current unknowns

ChoiceBenefitCost or unknown
Provider-neutral immutable messagesLog, UI, replay, and requests share one factEvery new block must land across all consumers
Raw chunks plus semantic messagesSupports both streaming observation and final meaningProjections must identify attempts and replacements correctly
Prepared one-shot callDefends against registration HMR and request-config driftDoes not yet close over adapter-internal config generation
Retries in the Agent LoopAttempts, waits, cancellation, and cost become recordableAlways mode is unbounded; same-Step identity exposes an accounting gap
Direct and library-backed adaptersCombines one high-fidelity path with broad coverageError detail, reasoning, usage, stop, and image capabilities are asymmetric
No implicit failoverProvider switching cannot silently alter semantics or costDeployments must compose an explicit fallback policy
  • The complete built-in provider and model catalog of the pinned pi-ai 0.82.1 dependency remains unverified because dependencies are absent.
  • No real-API experiment has compared reasoning, cache, and error fidelity between the two paths for the same model.
  • The dynamic-configuration generation window has not been reproduced through real settings HMR.
  • There is no standard event, idempotency, or cost protocol for automatic provider failover.
  • The upstream retry documentation needs correction to the same-Step attempt semantics.
Verification status

This chapter reviewed production source, types, and test source line by line, and labels test conclusions as contracts defined by tests. Dependencies are not installed in the upstream checkout, so this study did not run upstream tests and does not present source-derived inferences as runtime observations.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.