DeepSeek Harness: Declaring Dependencies: inject
Last updated: 2026-08-31
Plugins aren't islands — most plugins need services provided by other plugins. The inject array is Cordis's dependency declaration mechanism, ensuring dependencies load before their consumers and eliminating "service doesn't exist" runtime errors.
📋 Prerequisites: Completed 11-first-plugin.md, understand apply and Context
1. What You'll Learn
injectarray for declaring dependencies- Built-in service list: tools, llm, sessions, fs, shell, etc.
- Dependency loading order guarantees
- Optional dependencies
inject: ['tools', 'llm?'] - Circular dependency detection
- Underlying dependency injection mechanism
2. inject Array for Declaring Dependencies
(1) ▶ Example 1
import { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool'
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools and ctx.llm are guaranteed to be ready when apply is called
ctx.logger.info('tools ready:', !!ctx.tools)
ctx.logger.info('llm ready:', !!ctx.llm)
}
The inject array lists all service names the plugin needs. The framework ensures these services are registered before apply is called.
(2) ▶ Example 2
export default {
name: 'my-tool',
inject: ['tools', 'llm'],
apply(ctx: Context) {
// ...
}
}
(3) ▶ Example 3
export default class MyPlugin {
static name = 'my-tool'
static inject = ['tools', 'llm']
constructor(private ctx: Context) {
// ...
}
}
(4) Consequences of Not Declaring inject
// ❌ Not declaring inject, using services directly
export function apply(ctx: Context) {
ctx.tools.register(...) // Runtime error: ctx.tools may not exist
}
Without declaring inject, the plugin may load before dependent services are registered, causing ctx.tools to be undefined.
3. Built-in Service List
(1) Core Services
DSH provides the following services through built-in plugins:
| Service Name | Provider | Function |
|---|---|---|
tools |
dsh-core | Tool registration and execution |
llm |
dsh-plugin-llm | LLM adapter |
sessions |
dsh-core | Session management |
fs |
dsh-plugin-fs | File system operations |
shell |
dsh-plugin-shell | Shell command execution |
sandbox |
dsh-plugin-sandbox | Sandbox environment |
search |
dsh-plugin-search | Code search |
trajectory |
dsh-core | Log recording |
(2) Service Access Method
After declaring inject, access services via ctx.serviceName:
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools — Tool service
ctx.tools.register({
name: 'my_tool',
// ...
})
// ctx.llm — LLM service
const response = await ctx.llm.complete({
messages: [{ role: 'user', content: 'hello' }]
})
}
(3) Service Type Inference
TypeScript automatically infers service types on ctx based on inject:
// inject = ['tools'] → ctx.tools: ToolsService
// inject = ['llm'] → ctx.llm: LLMService
// inject = ['tools', 'llm'] → both ctx.tools + ctx.llm are typed
4. Dependency Loading Order Guarantee
(1) Topological Sorting
Cordis builds a dependency graph from all plugins' inject declarations, then loads in topological order:
graph LR
A[plugin-a<br/>inject: []] --> B[plugin-b<br/>inject: ['a']]
B --> C[plugin-c<br/>inject: ['a', 'b']]
Loading order: A → B → C
(2) Automatic Ordering
You don't need to manually control loading order. Even if C appears before A in cordis.yml:
plugins:
plugin-c: ...
plugin-a: ...
plugin-b: ...
The framework still loads in A → B → C order.
(3) Parallel Loading
Plugins with no dependency relationships can load in parallel:
graph TB
A[plugin-a] --> C[plugin-c<br/>inject: a, b]
B[plugin-b] --> C
A and B can load simultaneously; C only loads after both complete.
(4) Loading Phases
Phase 1: Load plugins with no dependencies → [core, logger]
Phase 2: Load plugins depending on Phase 1 → [tools, sessions]
Phase 3: Load plugins depending on Phase 2 → [my-plugin, other-plugin]
...
5. Optional Dependencies
(1) Syntax
Append ? to a dependency name to make it optional:
export const inject = ['tools', 'llm?']
Meaning: tools is a required dependency (missing causes load failure), llm is optional (missing still loads normally).
(2) Accessing Optional Dependencies
export const inject = ['tools', 'llm?']
export function apply(ctx: Context) {
// tools always exists
ctx.tools.register(...)
// llm may not exist
if (ctx.llm) {
ctx.llm.complete(...)
} else {
ctx.logger.warn('llm not available, skipping LLM features')
}
}
(3) Optional Dependency Use Cases
| Scenario | Required/Optional | Reason |
|---|---|---|
| Tool registration must use tools | Required | Core functionality |
| LLM capability enhancement | Optional | Works without it |
| Sandbox functionality | Optional | Not all environments have sandbox |
| Logging service | Required | Infrastructure |
(4) Runtime Detection
export const inject = ['tools', 'search?']
export function apply(ctx: Context) {
ctx.tools.register({
name: 'smart_search',
async execute(params) {
if (ctx.search) {
return ctx.search.query(params.query)
}
return 'search service not available'
}
})
}
6. Circular Dependency Detection
(1) What Is a Circular Dependency
A depends on B, and B depends on A:
A inject: ['B']
B inject: ['A']
This creates a deadlock: A waits for B, B waits for A — neither can load.
(2) Cordis's Detection Mechanism
The framework checks the dependency graph at startup and immediately reports circular dependencies:
Error: Circular dependency detected:
plugin-a → plugin-b → plugin-a
Please review your inject declarations.
(3) Resolving Circular Dependencies
Solution 1: Extract Shared Dependencies
Before: A → B → A
After: A → C, B → C
Extract the logic both A and B need into C.
Solution 2: Decouple with Events
// A doesn't directly depend on B, but listens for events
export const inject = []
export function apply(ctx: Context) {
ctx.on('b/ready', (bService) => {
// A uses B's capabilities without declaring dependency
})
}
Solution 3: Use Optional Dependencies
// A optionally depends on B
export const inject = ['B?']
export function apply(ctx: Context) {
if (ctx.B) {
// Use B
}
}
(4) Three-Node Cycles
A → B → C → A
Cordis can also detect multi-node cycles. The error message shows the complete chain.
7. Dependency Injection Underlying Mechanism
(1) Service Registration and Discovery
// Provider plugin registers service
ctx.provide('tools', toolsInstance)
// Consumer plugin discovers service
const tools = ctx.get('tools')
(2) inject and apply Timing
sequenceDiagram
participant F as Framework
participant P as Provider Plugin
participant C as Consumer Plugin
F->>P: Load Provider
P->>F: apply() → Register 'tools' service
F->>C: Check inject ['tools'] ✅ Ready
F->>C: Call apply()
C->>F: ctx.tools available
(3) When Dependencies Aren't Ready
sequenceDiagram
participant F as Framework
participant C as Consumer Plugin
F->>F: Check inject ['tools'] ❌ Not ready
F->>C: Plugin enters pending state
Note over F: Waiting for tools service registration
F->>F: tools service registered
F->>C: Re-check ✅ → Call apply()
Plugins aren't discarded when dependencies are missing — they enter a pending state and automatically activate when dependencies become ready.
(4) Type-Safe Dependency Injection
// Framework internal type mapping
interface Context {
tools: ToolsService // When inject includes 'tools'
llm: LLMService // When inject includes 'llm'
sessions: SessionService // When inject includes 'sessions'
// ...
}
TypeScript's conditional type mechanism automatically extends ctx's type definition based on the inject array, ensuring compile-time type safety.
❓ FAQ
bash pnpm dsh web --patch --dump-config # Or at runtime: ctx.logger.info(Object.keys(ctx.services)) import is TypeScript's static module reference, determined at compile time. inject is runtime service dependency, resolved by the Cordis framework during plugin loading. They complement each other: import brings in types and utility functions, inject declares runtime service dependencies.📖 Summary
- The
injectarray declares a plugin's runtime dependencies; the framework guarantees dependencies load before apply - Built-in services: tools, llm, sessions, fs, shell, sandbox, search, trajectory
- Append
?to a dependency name for optional dependencies; plugins load normally even if missing - Cordis automatically detects circular dependencies and reports errors; resolve by extracting shared dependencies / decoupling with events / using optional dependencies
- When dependencies are missing, plugins enter pending state and automatically activate when ready
- inject is declarative dependency — never manually control loading order
📝 Exercises
1. ⭐ Basic: Write a plugin declaring inject: ['tools'], register a simple tool using ctx.tools.register in apply. Start and verify the tool is available.
2. ⭐⭐ Intermediate: Write a plugin declaring inject: ['tools', 'llm?']. When llm is available, the tool calls LLM for enhancement; when unavailable, it returns degraded results. Test both scenarios.
3. ⭐⭐⭐ Challenge: Deliberately create two mutually dependent plugins A (inject: ['B']) and B (inject: ['A']), observe Cordis's circular dependency error. Then rewrite using event decoupling to eliminate the circular dependency, verifying both plugins load normally.