Capability Seams & LLM Adapter

Capability Seams & LLM Adapter

In One Sentence

Every replaceable capability upstream consists of three roles: Definition (the interface contract), Provider (a concrete implementation), and Consumer (uses the contract without depending on the implementation); the LLM capability is the prime example — through the unified LLMAdapter interface and StreamChunk protocol, dsh-go can attach any OpenAI-compatible endpoint without touching the upper loop.

Three Capability Roles

Service Definition, Service Provider, and Consumer together form a capability seam — a boundary where the implementation can be swapped without changing consumers:

RoleResponsibilitydsh-go counterpart
Definitioninterface and data contractGo interface + request/result structs
Providersupply and register an implementationimplement interface, reg.Put at wiring
Consumerlook up by key/interface, never import implreg.Get or injected interface

The key constraint: consumers depend on the abstraction. The Agent loop only knows it holds an LLMAdapter, not whether it is DeepSeek, a local model, or a test stub.

1type LLMAdapter interface {
2    Name() string
3    Chat(ctx context.Context, req ChatRequest, cb func(StreamChunk)) (Usage, error)
4}
5func NewAgent(llm LLMAdapter) *Agent { ... }

LLM Adapter: Any Model

To attach your own endpoint, implement two methods of pkg/llm.LLMAdapter:

  • Name() returns the adapter id;
  • Chat(ctx, req, cb) starts a streaming conversation; each chunk goes through cb, and it finally returns Usage.

The adapter translates the unified ChatRequest into the target wire format and the endpoint’s SSE stream back into unified StreamChunks. The upper layer is thus fully model-agnostic.

StreamChunk Protocol

pkg/llm.StreamChunk uses a Kind field for four chunk types:

KindPayload
ChunkTexttext delta
ChunkReasoningreasoning delta
ChunkToolCalltool-call delta (args may arrive in pieces)
ChunkDonestream finished

Error Handling in Streams

  • Chunk-level tolerance: one bad chunk must not lose received text;
  • Error chain: pkg/llm/errorchain.go wraps network, HTTP, and cancellation errors into a discriminable chain (errors.Is/As);
  • Retry: pkg/llm/retry.go only backs off on retryable errors (timeout, 429, 5xx); deterministic failures (4xx) return immediately;
  • Cache: pkg/cache serves deterministic requests to cut cost and latency.

Input Boundary Validation

External data is validated before entering the core: attachment.DecodeCanonicalBase64 rejects non-canonical base64 and NewLimiter bounds image-transform concurrency; feedback.ValidateRating/ValidateNote constrain rating and note; skills.IsName/RenderContent enforce the kebab grammar and escape embedded text; mcp.PublicToolName derives mcp__server__raw and appends an identity hash on lossy normalization to prevent collapse.

Anonymous Identity

telemetry.GetOrCreateAnonymousID mints a random UUID per Harness home, persisted as .anonymous-user-id, never derived from host or network. A write failure still returns a usable id.

Why the Three Roles Matter

This is how “everything is a plugin, freely replaceable” lands: swap a Provider without touching Consumers; inject a stub in tests; compose multiple Providers behind a routing adapter. The cost is upfront interface design — an unstable Definition moves every Consumer.

Source Map

ConceptGo implementationUpstream TypeScript
Adapter contractpkg/llm/llm.go — LLMAdapterllm adapter
Streaming protocolpkg/llm/llm.go — StreamChunkstream chunk
Error chainpkg/llm/errorchain.go(dsh-go addition)
Retrypkg/llm/retry.go(dsh-go addition)
Capability registrypkg/registry/registry.goCordis service
Attachment admission/limiterpkg/attachment/admission.go, limiter.goattachment/admission
Feedback validationpkg/feedback/validate.gomessage-feedback/spec
Skill grammarpkg/skills/grammar.goskill/src/index
MCP public namepkg/mcp/publicname.gomcp-client/tools
Anonymous identitypkg/telemetry/anonymous.goidentity/anonymous-user-id

Next Steps