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.

💡 Tip: Service's core value is "stateful services" — instance properties hold state, methods expose APIs, and other plugins use them after declaring inject dependencies. If your plugin only registers tools and listeners, the function form is simpler.

📋 Prerequisites: Completed 14-inject.md and 17-fiber.md

1. What You'll Learn


Seam Three Roles

2. Service Class Definition

(1) ▶ Example 1

TYPESCRIPT
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:

▶ Example 2: Cache Service

TYPESCRIPT
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:

TYPESCRIPT
super(ctx, 'cache')
// → Other plugins access this service instance via ctx.cache

(2) Naming Conventions

TYPESCRIPT
// ✅ 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:

TEXT 📖 Display only
Plugin A registers 'cache' → CacheServiceA
Plugin B registers 'cache' → CacheServiceB
→ Final ctx.cache = CacheServiceB

(4) Accessing Services

TYPESCRIPT
// 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:

TYPESCRIPT
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

TYPESCRIPT
static inject = ['fs', 'cache?']

Same syntax as function plugins; ? means optional.

(3) Service Initialization Timing

100%
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

TYPESCRIPT
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:

TYPESCRIPT
// 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:

TYPESCRIPT
// 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

TYPESCRIPT
// 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

100%
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:

TYPESCRIPT
// In the plugin's type declaration file
declare module '@deepseek-ai/cordis' {
  interface Context {
    cache: CacheService
  }
}

(2) ▶ Example 2

TYPESCRIPT
// 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

TYPESCRIPT
// 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

TYPESCRIPT
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

TEXT 📖 Display only
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:

TYPESCRIPT
// 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:

TYPESCRIPT
// 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

Q Can a Service register multiple service names?
A Technically you could call super multiple times in the constructor, but it's not recommended. A Service should register one service name — single responsibility.
Q What is Service's this.ctx?
A 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.
Q How to write Service destruction logic?
A Override the dispose() method: typescript export default class DbService extends Service { private pool: Pool dispose() { this.pool.end() }
Q Is there a difference between ctx.provide() in function plugins and Service?
A Functionally equivalent. The difference is Service has class instances and inheritance support; ctx.provide() just manually registers an object.
Q Can a Service depend on another Service?
A Yes. Declare dependencies with static inject and access via ctx.serviceName in the constructor.
Q How to write type extensions for third-party Services?
A Declare 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


📝 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏