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

// File path: scratch-plugin/src/use-tools.ts
// 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

// File path: scratch-plugin/src/metrics-service.ts
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

// File path: scratch-plugin/src/use-metrics.ts
// 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

// File path: scratch-plugin/src/metrics-service.ts
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

// File path: scratch-plugin/src/optional-dep.ts
// 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 typeSyntaxBehavior when a service is absentApplicable scenarios
Required dependencyexport const inject = ['tools']The plugin is not loaded and remains PENDINGIt cannot work normally without this service
Optional dependencyOmit inject, use ctx.get('metrics') to queryThe plugin loads as usual, ctx.get() returns undefinedThe 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:

  1. Plugins that depend on it will be automatically disposed (releasing resources).
  2. 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:

  1. In what form do services exist on ctx? Which parameter determines the service name?
  2. How to give ctx.metrics type hints?
  3. What is the difference in code between required and optional dependencies?
other extensions