跪拜 Guibai
← All articles
DeepSeek · Agent · TypeScript

Building a Mini DeepSeek Agent Runtime from Scratch with Cordis

By minorcell ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Cordis's "everything is a plugin" model turns agent runtimes into composable assemblies where tools, prompts, and logging are independently swappable modules rather than hardcoded features. For developers building or customizing AI agents, this architecture means adding a new tool or modifying the system prompt requires writing a new plugin, not refactoring a monolith.

Summary

The tutorial constructs a mini version of the DeepSeek Harness runtime using the Cordis framework, where every capability—from LLM calling to tool execution—is an independent plugin. It starts with a single "hello world" plugin and progressively adds a chat service, a tool registry, and three concrete tools (bash, fetch, file search) that register themselves via dependency injection. A prompt plugin demonstrates how to modify system instructions through waterfall events without touching agent code, and a tracer plugin logs tool calls by listening to broadcast events, showing how observability can be added without coupling.

The Agent loop itself is a service that builds its prompt from an event chain, calls the model, parses JSON tool-call responses, executes tools, and feeds results back until a final answer is reached. The entire harness is assembled by mounting eight plugins in a flat list; startup order is derived automatically from declared dependencies. The piece closes by contrasting Cordis's lifecycle-level plugin model with Express's request-level middleware, and honestly catalogs what the mini version still lacks compared to the production Harness—streaming, conversation memory, native function calling, and security governance.

Takeaways
A Cordis application is a single Context; plugins are mounted onto it and can take the form of functions, objects, or Service subclasses.
Services register capabilities on the context by name, letting other plugins discover them through TypeScript declaration merging rather than direct imports.
Tools are independent plugins that declare a dependency on the tools registry via `inject: ['tools']`; the framework resolves load order automatically.
Waterfall events allow plugins to wrap and modify values like the system prompt by calling `next()` to get the downstream result and returning a transformed version.
Broadcast events decouple observability: a tracer plugin can log every tool call by listening to `agent/step` without the agent service knowing it exists.
The Agent loop parses JSON from the model response, executes tools when a `tool` key is present, feeds results back as user messages, and stops when an `answer` key appears or max turns are reached.
Tool failures are returned as strings to the model rather than thrown as exceptions, letting the model adjust its strategy instead of crashing the loop.
The full harness is assembled by mounting eight plugins in a flat list; the real production version adds streaming, conversation memory, native function calling, and security plugins.
Conclusions

The tutorial's design choice to use a text JSON protocol instead of native function calling is a deliberate trade-off: it makes the mechanism transparent and debuggable, but the author openly notes the model sometimes mixes reasoning text with JSON, revealing the fragility of hand-rolled parsing in agent loops.

Cordis's two-layer middleware model—lifecycle-level plugins and dispatch-level event chains—is a useful mental model for understanding why the framework feels like Express but operates at a fundamentally different scope.

The pattern of returning tool errors as strings to the model rather than throwing exceptions is a practical design choice that treats the model as a decision-maker capable of recovering from failures, rather than a brittle pipeline that crashes on any misstep.

Concepts & terms
Cordis Service
A plugin that extends the Service class and registers a named capability on the Context. Other plugins access it via `ctx.serviceName` rather than importing the implementation directly, enabled by TypeScript declaration merging.
Waterfall event
A Cordis event pattern where each listener receives the current value and a `next()` continuation. Calling `await next()` yields the downstream result, which the listener can wrap, modify, or short-circuit before returning upstream.
Dependency injection via `inject`
A plugin declares its service dependencies with an `inject` array (e.g., `inject: ['tools']`). The Cordis framework waits for those services to be ready before loading the plugin, deriving startup order automatically.
Agent loop
The core runtime cycle: the model receives a prompt with available tools, outputs a JSON decision (tool call or final answer), the harness executes any requested tool and feeds the result back as a new message, repeating until the model signals completion or a turn limit is reached.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗