A service is how DeepSeek Harness plugins share capabilities: built-in services are mounted on ctx (ctx.tools, ctx.llm, ctx.agents), and any plugin can expose its own capabilities as a service for others to inject. This tutorial covers four things: how to consume services, how to provide them, how the framework protects you when a service disappears, and how to isolate service instances with isolate.
What a service is: capabilities shared between plugins
A single plugin can only do so much alone. Harness factors reusable capabilities into services — provided by one plugin, registered on the Context, and consumed by other plugins that declare a dependency on them.
The built-in services are the most familiar examples: ctx.tools is the tool runtime (ToolRuntime), ctx.llm provides LLM access, and ctx.agents manages agents. They are mounted when Harness starts, so your plugins only need to declare a dependency to use them.
The key point: providers and consumers never know about each other — they are coupled only by a name on the Context (such as "tools" or "metrics"). Plugins can be developed and replaced independently, which is what makes the Harness ecosystem composable.
Declare required dependencies with inject
The safest way to consume a service is to declare it as a required dependency. Export an inject array listing the service names you need:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'usage-reporter'
export const inject = ['tools']
export function apply(ctx: Context) {
// when apply runs, the tools service is guaranteed to be ready
ctx.tools.register(/* ... */)
}
The framework guarantees that when apply runs, every injected service is ready; if a service is not ready yet, the plugin waits rather than running with an unmet dependency.
Optional dependencies: query on demand with ctx.get()
Not every dependency is required. If your plugin reports an extra metric when a metrics service exists but works fine without it, leave it out of inject and query it at the use site with ctx.get():
import type { Context } from '@deepseek-ai/cordis'
export const name = 'metrics-insights'
export function apply(ctx: Context) {
// no inject: metrics is optional, queried only when used
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
ctx.get() returns the instance when the service exists and undefined otherwise, which is why the call is followed by optional chaining ?.. The plugin loads normally and is never blocked by a missing enhancement.
Provide a service: extend Service
Now the reverse: expose your plugin's capabilities to others. Write a class extending Service and register it on the Context with super(ctx, name) in the constructor:
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
static inject = ['llm']
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number) {
// implement metric recording here
}
}
Three things matter here: super(ctx, 'metrics') decides the name consumers use to access the service; static inject declares the service's own dependencies (in this example, metrics depends on llm); and the public methods on the class (such as record) are the service's API.
Consumers simply add inject = ['metrics'] and call ctx.metrics.record(...) — indistinguishable from using a built-in service.
Typing via declaration merging
ctx.metrics is untyped by default. Extend the Context interface with TypeScript declaration merging so consumers get type hints and checking:
import type { MetricsService } from './metrics-service'
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
}
Put this declaration in the service module (or its type entry point), and every plugin that imports the module automatically gets the ctx.metrics type. This is the standard Cordis service pattern — it is exactly how the built-in services get their types too.
Service disappearance and automatic recovery
Required dependencies come with one more guarantee: if a service's provider unloads and the service vanishes from the Context, the framework automatically unloads every plugin that depends on it; when the service returns, it reloads them automatically.
This means you do not listen for "service gone" events, and you do not check whether a service exists before every call — the framework guarantees your plugin either runs with the service intact or is paused entirely. It never calls into a nonexistent service.
Automatic unload still follows the standard cleanup flow: cleanup functions you registered with ctx.effect run as usual:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'metrics-consumer'
export const inject = ['metrics']
export function apply(ctx: Context) {
const timer = setInterval(() => {
ctx.metrics.record('heartbeat', 1)
}, 5000)
// this cleanup also runs when the plugin is auto-unloaded due to service loss
ctx.effect(() => {
return () => clearInterval(timer)
})
}
Service isolation with isolate
By default, a same-named service shares one instance globally. Sometimes you want two groups of plugins to each own a separate service instance — for example, two groups both using the Bash plugin but with different timeout values. Add isolate to the groups in cordis.yml:
- insert:
- id: group-a
isolate: { shell: true }
- name: '@deepseek-ai/dsh-bash-local'
group: group-a
config:
timeoutMs: 5000
- id: group-b
isolate: { shell: true }
- name: '@deepseek-ai/dsh-bash-local'
group: group-b
config:
timeoutMs: 30000
isolate: { shell: true } means plugins in that group see a private shell service instance owned by the group. The 5-second Bash in group-a and the 30-second Bash in group-b never interfere — they do not inject the same instance.
Built-in services and where to find their APIs
Harness's built-in services (tools, llm, agents, and more) evolve between releases, so memorizing a method list is the wrong approach. Each service subsystem reference page generates the service's names, methods, and source locations, and together with the TypeScript interfaces (jump to definition in your editor) they are the most reliable API documentation available.
Services and dependency injection FAQ
How do I choose between inject and ctx.get?
Ask whether the dependency is "cannot live without it" or "nice to have". Use inject for required dependencies: the framework guarantees the service is ready, and unloads and reloads your plugin automatically if the service disappears — at the cost of the plugin not loading while the service is absent. Use ctx.get() for optional dependencies: the plugin loads normally, queries the service at the use site, and falls back with optional chaining.
What happens to my plugin when a required service disappears?
The framework automatically unloads your plugin (cleanup functions registered with ctx.effect still run), then reloads it once the service returns. You write no listeners or retry logic — the plugin never runs while the service is missing.
What problem does isolate solve?
It gives each plugin group its own instance of a same-named service. The classic case is running one plugin with two coexisting configurations: two groups each run @deepseek-ai/dsh-bash-local with different timeoutMs values, and the shell instances they inject are invisible to each other.
Where do I find the full method list of a built-in service?
Each service subsystem reference page generates the service names, methods, and source locations for the current version. The most direct route is "go to definition" on ctx.<service-name> in your editor — the TypeScript interface is the authoritative API for the version you have installed.