Skip to content

CH 19 · Three Plugin Forms: Function / Object / Class

Word count~2,870 wordsTime~20 minPrereqCH 18 (Your First Plugin)LevelReproducible

Chapter Goal

CH 18 was the most common way to write a plugin. This chapter fully covers the three plugin forms (function, object, class) in one go: what each looks like, when to use which, and why the class form is "opening a public window" — it's the key to plugin collaboration. After reading this chapter, you'll know which form a plugin should use, instead of just being able to use one template.

The Three Forms at a Glance

One table for the overview, details broken down one by one below:

FormWhat it looks likeOne-line understandingWhen to use
Functionexport function apply(ctx) {}You go handle a thing yourselfDefault choice, ninety percent of cases are enough
Objectexport default { name, inject, apply(ctx) }Pack the name and flow togetherWhen you want to give the plugin some static declarations
Classexport default class extends Service {}Open a public window, anyone can come handle thingsWhen you want other plugins to call your capabilities

One judgment is enough: use function to add capabilities to the Agent; use class to let other plugins depend on you. The object form is in the middle, functionally basically equal to the function, just with more static metadata like name.

Function Form: You Already Know It

CH 18's hello-plugin is the function form:

js
export function apply(ctx) {
  // Register your capabilities here
}

The framework calls apply(ctx) when loading the plugin, handing you the context. The vast majority of plugins — register tools, listen to events, mount timers — this one is enough. Until you're sure you'll be depended on by other plugins, always write the function form first.

Object Form: Bundle the Name and Flow

The object form is putting name, inject, apply in one object and exporting together:

js
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx) {
    // Here ctx.tools is guaranteed to be available
  },
}

It's almost equivalent to the function form, the only difference is organizing the static declarations (name, dependencies) and apply into a "packaged spec". When to use? When you want your plugin's "who am I, what services do I need, what do I do" to be clear at a glance, the object form is cleaner. Functionally there's nothing the function form can't do.

Class Form: When the Plugin Wants to "Provide a Service"

This section is the focus of this chapter. First establish a concept:

A Service is a named capability mounted on ctx. You use services every day — ctx.tools (tools), ctx.llm (model), ctx.agents (subagents) are all services. Any plugin can provide its own services for other plugins to call.

Function/object form is "go handle things yourself"; the class form is open a public window, anyone can come to you to handle things. An analogy: the function form is like you going to a service hall to handle your own business; the class form is like you open a window in the hall, others (other plugins) come to your window to submit forms and get results.

Provider: Write a Service Subclass

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')  // Register a service called metrics
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

Two key points:

  • extends Service + super(ctx, 'metrics'): mount the name metrics on ctx, other plugins can access it via ctx.metrics.
  • record(event, value) is the method this service provides externally — others calling ctx.metrics.record(...) will arrive here.

Consumer: Declare Dependencies with inject

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

inject: ['metrics'] declares "I want to use the metrics service". The framework guarantees: when apply runs, the services declared in inject are definitely ready. If the service isn't ready, your plugin will wait and won't run early.

One Diagram to Understand the Collaboration

The provider mounts the service on ctx, the consumer declares dependencies and calls directly. Built-in capabilities like tools, llm, agents are essentially the same mechanism — when you use them, you're a "consumer".

Hands-on: Make Two Plugins Actually Talk

Above was all concept. Now let's hands-on write a provider and a consumer and let them actually talk. In a workspace where you want to put plugins (first cd there) create a directory:

powershell
New-Item -ItemType Directory -Path "service-demo\src" -Force

Step 1: Install Dependencies in the Plugin Directory

The class form needs to import the framework's @deepseek-ai/cordis package. Your workspace doesn't have it, loading directly will report Cannot find package '@deepseek-ai/cordis'. So go into the plugin directory and install it first:

powershell
cd "E:\software-workspace\DeepSeek harness demo\service-demo"   # replace with your directory
npm init -y
npm install @deepseek-ai/cordis

After installation, modify package.json and add a line "type": "module" to the root object — otherwise Node will guess the module format every time it loads the plugin and spam warnings:

json
{
  "name": "service-demo",
  "version": "1.0.0",
  "type": "module"
}

Step 2: Write the Provider

Create service-demo\src\provider.js:

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

Step 3: Write the Consumer

Create service-demo\src\consumer.js:

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

Step 4: Declare and Verify

Create service-demo\cordis.yml (replace the path with your own, note spaces must be written as %20):

yaml
- insert:
    - id: provider
      name: 'file:///E:/your-workspace/service-demo/src/provider.js'
    - id: consumer
      name: 'file:///E:/your-workspace/service-demo/src/consumer.js'

Use headless for quick verification:

powershell
cd your-workspace-directory
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"

In the output you'll see:

text
[metrics] plugin_loaded 1
hi

[metrics] plugin_loaded 1 is the consumer calling the provider's method — your two plugins actually talked. At this point, you've personally verified all three forms: function, object, class.

The yellow line [metrics] plugin_loaded 1 in the terminal output is the consumer calling the provider's method via ctx.metrics.record(...) — the two plugins actually talked.

Let dsh Do It: One Prompt Does It

The directories, dependencies, two files above were all manually created by you. As usual, writing plugins is something dsh can do itself — and it knows "how services should be defined, how dependencies should be injected" better than you.

Send this prompt directly in the Web UI's input box:

text
In my current workspace, help me write a "service form" plugin example: a service-providing plugin (Service subclass) and a plugin that uses it (inject depending on it), make them actually talk. First read the official dsh plugin dev docs to understand how to define services, how to inject dependencies, implement per the official spec, verify it can load and call normally, and finally tell me the result and where the files are.

It will go look up the official docs itself, install dependencies itself, write two plugins itself, verify whether they can talk to each other itself. You just verify what it wrote.

Common Pitfalls

ProblemWhat's happeningHow to handle
Reports Cannot find package '@deepseek-ai/cordis'Plugin directory didn't install dependenciesnpm install @deepseek-ai/cordis in the plugin directory
A bunch of MODULE_TYPELESS_PACKAGE_JSON warningspackage.json didn't declare module typeAdd "type": "module"
Consumer can't get the serviceService name mismatchCheck the name in super(ctx, 'xxx') and inject: ['xxx'] are exactly the same
Class plugin fails to loadNo extends Service or didn't call superThe class form must inherit Service and super(ctx, 'service-name')
ctx.metrics is undefinedConsumer didn't declare injectIn the consumer write export const inject = ['metrics']

What you learned in this chapter

You pass if you can complete the items below:

  • [ ] State what each of the three forms looks like, and when to use which
  • [ ] Know that service = a named capability mounted on ctx; tools/llm/agents are all services
  • [ ] Write a Service subclass as the provider, super(ctx, 'name') to register the service
  • [ ] Write an inject consumer, call directly via ctx.service-name.method() in apply
  • [ ] Know that the class form plugin must install the @deepseek-ai/cordis dependency in the plugin directory first

Open Source · MIT · Community Driven