DeepSeek Harness Services and Dependencies: Service Base Class and Type Declarations
In the previous chapters, we have been using built-in services such as ctx.tools, ctx.llm, and ctx.agents.
In this chapter, we look from the service provider's perspective at how to useService base classprovide your own named service and give consumers full type hints.
What is a service?
In Harness, tools, llm, and agents are all services.
Services are named capabilities mounted on ctx; any plugin can provide services for other plugins to use.
Services: the capability that a plugin exposes to other plugins.
It occupies a stable ctx.<key> (such as ctx.tools, ctx.llm, ctx.sessions), and other plugins look up services by key rather than importing a specific implementation.
For example, ctx.tools is the tool runtime service, ctx.llm is the large model service, and ctx.agents is the agent service.
The service names, public methods, and source locations of built-in services are automatically generated by the repository into each service's subsystem page. When developing plugins, rely on these generated blocks and the TypeScript interfaces of the services, and do not depend on any hand-written static service manifest.
Using services: inject declarations
To use an existing service, declare inject in the plugin.
The framework guarantees: when apply executes, all services declared by inject are already ready.
Example
// Declare dependency on the tools service
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools is guaranteed to exist and be ready
ctx.tools.register(/* ... */)
}
If a service is not ready yet, your plugin will wait and will not execute apply.
Providing a service: the Service base class
Derive a subclass from the Service base class and call in the constructorsuper(ctx, 'service name')Register naming service.
Example
import { Service, type Context } from '@deepseek-ai/cordis'
// Services can also depend on other services, declared with static inject
export default class MetricsService extends Service {
static inject = ['llm'] // This service requires the llm service to be ready first
constructor(ctx: Context) {
// The second parameter 'metrics' is the service name, mounted to ctx.metrics.
super(ctx, 'metrics')
}
// Public service method
record(event: string, value: number) {
// Record a metric here, e.g. tool call count
// The actual implementation writes to the metrics backend; omitted here
}
}
After loading this plugin, consumers can access it via ctx.metrics.
Example
// Consumer declares dependency on metrics service
export const inject = ['metrics']
export function apply(ctx: Context) {
// Call service method: record a tool call
ctx.metrics.record('example_tool_call', 1)
}
Type Declarations: declare module Merging
Use TypeScript declaration merging to give ctx.metrics the correct type.
This gives autocompletion when writing code and lets you catch misspelled service names at compile time.
Example
import { Service, type Context } from '@deepseek-ai/cordis'
// Declaration merging: tell TypeScript that Context has a metrics field
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService
}
}
export default class MetricsService extends Service {
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number) {
/* Implementation omitted */
}
}
declare module declaration merging is the standard TypeScript way to add types to an existing module.
Here, the type and implementation of ctx.metrics are written in the same file to ensure they don't drift apart.
Required dependencies and optional dependencies
Service dependencies are divided into two types: required and optional.
Required dependencies are declared with inject; if a service is absent, the plugin is not loaded at all.
Optional dependencies omit inject; at the place of use, usectx.get()The query may return undefined.
Example
// Required dependency: the plugin will not load when the service is absent
export const inject = ['tools']
// Optional dependency: omit inject, query with ctx.get() at the usage site
export function apply(ctx: Context) {
// ctx.get('metrics') may return undefined, use optional chaining to safely call
const metrics = ctx.get('metrics')
metrics?.record('example_plugin_loaded', 1)
}
| Dependency type | Syntax | Behavior when a service is absent | Applicable scenarios |
|---|---|---|---|
| Required dependency | export const inject = ['tools'] | The plugin is not loaded and remains PENDING | It cannot work normally without this service |
| Optional dependency | Omit inject, use ctx.get('metrics') to query | The plugin loads as usual, ctx.get() returns undefined | The service is optional; the plugin must work normally even when it is absent. |
When to use optional dependencies? When the service is optional and your plugin must work normally even in its absence.
Behavior when a service disappears
If a required service disappears during application runtime (for example, its provider is uninstalled), two things will happen:
- Plugins that depend on it will be automatically disposed (releasing resources).
- When the service reappears, the plugin is automatically reloaded.
This prevents plugins from calling services that no longer exist.
Summary self-test
Service subclasses provide named services via super(ctx, 'name'), declare module declaration merging fills in the types, inject declares required dependencies, and ctx.get() queries optional dependencies.
Test yourself:
- In what form do services exist on ctx? Which parameter determines the service name?
- How to give ctx.metrics type hints?
- What is the difference in code between required and optional dependencies?