DSH plugin capability roles: Definition, Provider, Consumer
When a DSH plugin capability is general enough, the official guidance splits it into three roles: a Service Definition for the contract, a Service Provider for the implementation, and a Consumer that exposes it. This is not architecture for its own sake — it makes swapping implementations a one-line configuration change, because callers only know the Definition. The test for splitting is equally clear: do the three roles need to evolve independently? If not, keep one package; if yes, split. Bash is the canonical example: dsh-shell is the definition, dsh-bash-local is the provider, and dsh-tool-bash is the consumer — together they form one seam.
DSH plugin capability roles: Definition, Provider and Consumer
Bottom line: the Definition owns the contract, the Provider owns the implementation, and the Consumer owns exposure; dependencies are one-way and the last two never depend on each other.
| Role | Responsibility | Depends on | Bash example |
|---|---|---|---|
| Service Definition | Service plus request/result types | Nothing | dsh-shell |
| Service Provider | Performs the capability | Definition | dsh-bash-local |
| Consumer | Exposes it as a tool or command | Definition | dsh-tool-bash |
- Callers know only the contract: a Consumer injects
shelland never cares whether execution is local or remote. - Swaps happen at composition: changing a Provider is one line in
cordis.yml; Definition and Consumer stay as they are. - Roles are not packages: they are logical divisions, so one package may hold several roles or be split into three.
Why split a DSH plugin capability into three packages
Bottom line: three payoffs — swappable providers, independent evolution, and decoupled dependencies.
- Swappable providers: one Definition can have several Providers selected by configuration, with the calling convention unchanged.
- Independent evolution: the Definition rarely changes once callers depend on it, Providers can optimize performance and safety, and Consumers can change how a capability is presented to the model.
- Decoupled dependencies: Provider and Consumer never depend on each other, so changes on one side cannot break the other.
- Clearer diagnosis: when something breaks, identify which role failed — contract mismatch, implementation error, or exposure problem.
Three steps to build a DSH plugin capability
Bottom line: write the Definition with request/result types first, then the Provider, then the Consumer; the order matters.
- Write the Service Definition:
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context { myCap: MyCapService }
}
export abstract class MyCapService extends Service {
constructor(ctx: Context) { super(ctx, 'myCap') }
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
export interface MyCapRequest { input: string }
export interface MyCapResult { output: string }
- Write the Service Provider:
class MyCapLocal extends MyCapService {
async execute(request: MyCapRequest): Promise<MyCapResult> {
return { output: request.input.toUpperCase() }
}
}
export const name = 'my-cap-local'
export function apply(ctx: Context) { ctx.plugin(MyCapLocal) }
- Write the Consumer as a model-facing tool:
export const inject = ['tools', 'myCap']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'my_cap',
description: 'Execute my capability.',
parameters: { input: { type: 'string', required: true } },
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
async execute(args) { return (await ctx.myCap.execute({ input: args.input })).output },
}))
}
- Compose both rows: the provider package and the consumer package, in
cordis.yml. - Verify swappability: replace the provider with another implementation and change only that row; the tool name and call shape stay identical.
Where to stop splitting a DSH plugin capability
Bottom line: do not split preemptively — only when roles truly evolve independently is the extra package cost worth paying.
- A simple tool plugin needs no split: there is no alternative implementation, so splitting only adds release and dependency overhead.
- One provider that will not change soon: keeping Definition and Provider together is simpler.
- Too many packages: installation, version alignment and debugging all get harder; make one package work first.
- Split signals: a second Provider is needed, or the Consumer must be reused across scenarios.
- Team boundaries: when different people own different roles, separate packages unblock release cadence.
DSH plugin composition and swaps at the seam
Bottom line: composition happens in configuration, and swapping a Provider is a configuration change rather than a code change.
- Mount by package name: list Providers and Consumers in order inside
cordis.yml. - Swap one row: comment out the old Provider and add a package with the same contract.
- Verify: call the tool once and confirm the result still matches the Definition's Result shape.
- Check the seam reference first: built-in capabilities already have seam splits, so look before inventing a new one.
DSH plugin troubleshooting around the three roles
Bottom line: ask which layer broke before asking where the code is wrong.
- "Service missing" — check that the Provider is mounted and
injectis spelled correctly. - Type mismatch — ensure request/result types exist in exactly one place: the Definition.
- Behavior changed after a swap — the implementation violated the contract; align it back to the Definition.
- A Consumer instantiating the implementation — that breaks the dependency direction and defeats swapping.
- Splitting too early — ship one package, then split when the need is real.
- Repair status, stated honestly: the roles, dependency direction and three-step example follow the official Three capability roles page; confirm
defineToolfields andServicesignatures against the official reference for your version.
Sources: Three capability roles, Services and dependencies, Capability seams.
FAQ
In DSH plugin design, the Service Definition owns the service contract and request/result types, the Provider implements the capability, and the Consumer exposes it to the model or other plugins. Callers depend only on the Definition, so swapping a Provider leaves the Definition and Consumer untouched.
DSH plugin roles need splitting only when they evolve or are replaced independently; splitting merely to look architectural is over-design. DSH plugin authors should ask whether the Provider and Consumer will actually change separately — if not, one package is easier to maintain.
Swapping a DSH plugin Provider normally changes one line of composition configuration; the Definition and Consumer stay the same. That is the core payoff of separating the three roles: replacing an implementation never touches the calling convention.
The DSH plugin Service Definition owns the request and result types, and both Provider and Consumer depend only on that package. A single type source prevents the two ends from drifting apart.
No: DSH plugin dependencies are one-way — Provider and Consumer each depend on the Definition, while Provider and Consumer never depend on each other. That direction is what makes swapping either role safe.
Related Terms
- Service Definition
- Service Definition is the contract package of a DSH plugin capability, declaring the Cordis service and its request and result types. Provider and Consumer both depend only on it, making it the slowest-changing layer.— https://deepseek-harness.github.io/deepseek-harness/develop/practice/
- Service Provider
- Service Provider is the implementation package of a DSH plugin capability and performs the actual work. One Service Definition can have several Providers, selected through composition configuration.— https://deepseek-harness.github.io/deepseek-harness/develop/practice/
- Consumer
- A Consumer exposes a DSH plugin capability to the model or other callers, usually as a tool or command. It depends only on the Service Definition, never directly on a Provider.— https://deepseek-harness.github.io/deepseek-harness/develop/practice/
- capability seam
- A capability seam is the overall boundary formed by the three roles of one capability; no single role is the seam. It determines where an implementation can be replaced.— https://deepseek-harness.github.io/deepseek-harness/reference/capability-seams
Sources
- DeepSeek Harness docs — Three capability roles (Definition / Provider / Consumer)· deepseek-harness.github.io
- DeepSeek Harness docs — Services and dependencies (ctx.plugin, inject, service registration)· deepseek-harness.github.io
- DeepSeek Harness docs — Capability seams reference· deepseek-harness.github.io