DeepSeek Harness: Services and Dependencies
Last updated: 2026-08-31
Function plugins register "doing" capabilities via apply, while Service plugins expose "providing" capabilities through classes. When your plugin needs to maintain internal state, expose callable APIs, or serve as a dependency base for other plugins, the Service base class is the best choice.
📋 Prerequisites: Completed 14-inject.md and 17-fiber.md
1. What You'll Learn
- Service class definition
- constructor(ctx, 'serviceName')
- static inject declarations
- Service registration and discovery
- Type-safe service access
- Service vs function plugin comparison
2. Service Class Definition
(1) ▶ Example 1
import { Service } from '@deepseek-ai/cordis'
export default class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'my-service')
}
}
The Service base class constructor takes two parameters:
ctx: Cordis contextserviceName: Service identifier; other plugins reference it by this name
▶ Example 2: Cache Service
import { Service, Context } from '@deepseek-ai/cordis'
export default class CacheService extends Service {
private cache = new Map<string, { value: any; expires: number }>()
constructor(ctx: Context) {
super(ctx, 'cache')
}
get(key: string): any {
const entry = this.cache.get(key)
if (!entry) return undefined
if (Date.now() > entry.expires) {
this.cache.delete(key)
return undefined
}
return entry.value
}
set(key: string, value: any, ttlMs: number = 60000): void {
this.cache.set(key, { value, expires: Date.now() + ttlMs })
}
delete(key: string): boolean {
return this.cache.delete(key)
}
clear(): void {
this.cache.clear()
}
}
(3) Auto-Registration
super(ctx, 'cache') in the Service constructor automatically registers the instance as the cache service. Other plugins declare inject: ['cache'] to use it.
3. constructor and Service Name
(1) Service Name Purpose
The service name is the key in the global registry:
super(ctx, 'cache')
// → Other plugins access this service instance via ctx.cache
(2) Naming Conventions
// ✅ Recommended: kebab-case or camelCase
super(ctx, 'cache')
super(ctx, 'rate-limiter')
super(ctx, 'metricsCollector')
// ❌ Not recommended
super(ctx, 'Cache') // Uppercase start
super(ctx, 'cache_service') // Underscores
(3) Service Name Conflicts
If two Services register the same name, the later one overrides the earlier:
Plugin A registers 'cache' → CacheServiceA
Plugin B registers 'cache' → CacheServiceB
→ Final ctx.cache = CacheServiceB
(4) Accessing Services
// Consumer plugin
export const inject = ['cache']
export function apply(ctx: Context) {
ctx.cache.set('user:1', { name: 'Alice' }, 300000)
const user = ctx.cache.get('user:1')
}
4. static inject Declarations
(1) Service Dependencies
Service classes declare their dependencies via static inject:
export default class DatabaseService extends Service {
static inject = ['fs']
constructor(ctx: Context) {
super(ctx, 'database')
}
async query(sql: string) {
const schema = await ctx.fs.readFile('schema.json')
// ...
}
}
(2) Optional Dependencies
static inject = ['fs', 'cache?']
Same syntax as function plugins; ? means optional.
(3) Service Initialization Timing
sequenceDiagram
participant F as Framework
participant FS as FS Service
participant DB as Database Service
participant Tool as Tool Plugin
F->>FS: Load → active
F->>DB: inject ['fs'] ✅ → constructor executes → active
F->>Tool: inject ['database'] ✅ → apply executes → active
(4) Using Dependencies in Constructor
export default class MyService extends Service {
static inject = ['tools']
private defaultTool: string
constructor(ctx: Context) {
super(ctx, 'my-service')
// ctx.tools is ready (inject guarantees it)
this.defaultTool = ctx.tools.getDefault()
}
}
5. Service Registration and Discovery
(1) Registration Mechanism
super(ctx, name) in the Service constructor triggers registration:
// Framework internal (pseudocode)
class Service {
constructor(ctx: Context, name: string) {
ctx.provide(name, this)
// Triggers activation of pending plugins depending on this service
}
}
(2) Discovery Mechanism
Other plugins declare dependencies via inject; the framework injects them when ready:
// Consumer plugin
export const inject = ['cache']
export function apply(ctx: Context) {
// ctx.cache auto-injected, type-safe
ctx.cache.set('key', 'value')
}
(3) Runtime Service Query
// Check if a service is registered
if (ctx.cache) {
ctx.cache.get('key')
}
// List all registered services
ctx.logger.info('available services:', ctx.serviceNames)
(4) Service Dependency Graph
graph TB
FS[fs Service] --> DB[database Service]
CACHE[cache Service] --> DB
DB --> TOOL1[tool-a Plugin]
DB --> TOOL2[tool-b Plugin]
CACHE --> TOOL1
6. Type-Safe Service Access
(1) TypeScript Type Extension
To give ctx.cache proper type hints, declare a type extension:
// In the plugin's type declaration file
declare module '@deepseek-ai/cordis' {
interface Context {
cache: CacheService
}
}
(2) ▶ Example 2
// cache-service.ts
import { Service, Context } from '@deepseek-ai/cordis'
export default class CacheService extends Service {
private cache = new Map<string, any>()
constructor(ctx: Context) {
super(ctx, 'cache')
}
get(key: string): any {
return this.cache.get(key)
}
set(key: string, value: any, ttl?: number): void {
this.cache.set(key, value)
}
has(key: string): boolean {
return this.cache.has(key)
}
delete(key: string): boolean {
return this.cache.delete(key)
}
clear(): void {
this.cache.clear()
}
}
// Type extension
declare module '@deepseek-ai/cordis' {
interface Context {
cache: CacheService
}
}
(3) Consumer-Side Type Safety
// Consumer plugin
import { Context } from '@deepseek-ai/cordis'
export const inject = ['cache']
export function apply(ctx: Context) {
ctx.cache.set('key', 'value') // ✅ Type correct
ctx.cache.invalid() // ❌ Compile error: method doesn't exist
ctx.cache.get('key').foo() // ⚠️ any type, needs further typing
}
(4) Generic Services
export default class CacheService<T = any> extends Service {
private cache = new Map<string, T>()
get(key: string): T | undefined {
return this.cache.get(key)
}
set(key: string, value: T): void {
this.cache.set(key, value)
}
}
7. Service vs Function Plugin Comparison
(1) Feature Comparison
| Dimension | Service Plugin | Function Plugin |
|---|---|---|
| Form | Class | Function/object |
| State management | Instance properties | Closure variables |
| Service exposure | super(ctx, name) auto-registers | ctx.provide() manual registration |
| Dependency declaration | static inject | export const inject |
| Lifecycle | Constructor/destruction | Apply/auto-cleanup |
| Inheritance | ✅ Supported | ❌ Not supported |
| Testability | ✅ Easy to mock | ⚠️ Requires mocking ctx |
| Code volume | More | Less |
(2) Selection Guide
Choose Service when:
→ Need to expose APIs to other plugins
→ Need to maintain stateful data
→ Need inheritance and reuse
→ Serving as a base dependency for multiple plugins
Choose function plugin when:
→ Only registering tools and listeners
→ Stateless or simple state
→ Minimal code, quick development
→ Don't need to be depended on by other plugins
(3) Mixed Usage
Both forms can coexist in a project:
// CacheService — Service plugin
export default class CacheService extends Service { ... }
// CacheTool — Function plugin, consumes CacheService
export const inject = ['cache', 'tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'cache_get',
// ...
async execute({ key }, ctx) {
return ctx.cache.get(key)
}
}))
}
(4) Migrating from Function Plugin to Service
When a function plugin grows complex, migrate it to a Service:
// Before: Function plugin
export function apply(ctx: Context) {
const cache = new Map()
ctx.provide('cache', {
get: (k) => cache.get(k),
set: (k, v) => cache.set(k, v)
})
}
// After: Service plugin
export default class CacheService extends Service {
private cache = new Map()
constructor(ctx: Context) {
super(ctx, 'cache')
}
get(k: string) { return this.cache.get(k) }
set(k: string, v: any) { this.cache.set(k, v) }
}
❓ FAQ
this.ctx is the ctx parameter received in the constructor, automatically saved by the Service base class. It's the plugin's context, providing access to all injected services.dispose() method: typescript export default class DbService extends Service { private pool: Pool dispose() { this.pool.end() } static inject and access via ctx.serviceName in the constructor.declare module '@deepseek-ai/cordis' in a .d.ts file and extend the Context interface. Ensure this declaration file is included by the TypeScript compiler.📖 Summary
- Service is Cordis's class-based plugin form;
super(ctx, 'serviceName')auto-registers the service static injectdeclares Service dependencies, guaranteeing services on ctx are ready in the constructor- Type extensions via
declare modulelet TypeScript recognizectx.serviceNametypes - Service suits stateful, API-exposing, inheritance-needed scenarios; function plugins suit stateless, lightweight scenarios
- Both forms can mix: Service provides base services, function plugins consume services and register tools
📝 Exercises
1. ⭐ Basic: Write a RateLimiterService with a check(key): boolean method (max 60 calls per minute). Register it as the rate-limiter service and use it in another function plugin via inject.
2. ⭐⭐ Intermediate: Add a type extension (via declare module) for CacheService so that ctx.cache.get() returns properly typed values in consumer plugins. Write a consumer plugin and verify TypeScript type-checking passes.
3. ⭐⭐⭐ Challenge: Implement a MetricsService that collects tool call counts and timing statistics. It depends on the tools service and records data before and after tool execution. Provide a getStats(): Record<string, { count, avgMs }> method. Register a metrics command in the Web UI to display statistics.