← Writing

DeepSeek Harness

Why Does Replacing One Agent Component Mean Taking Everything Apart?

Follow a filesystem backend replacement to see how DeepSeek Harness combines service interfaces, dependency identity, scopes, and cleanup into replaceable Agent components.

Imagine you already have an Agent that reads a project, edits files, and runs checks. At first, everything runs locally and the arrangement is straightforward. Later, you want file operations on a remote machine, a different model provider, and an independent set of tools for one conversation.

It sounds like replacing a few components. Once you begin, however, the file tool imports the local implementation directly, model requests are buried inside the loop, and unloaded plugins leave listeners behind. One replacement spreads through the system.

The final OpenClaw article followed a plugin into the runtime. DeepSeek Harness pushes that question further: if model adapters, the tool registry, the session log, and even the Agent loop are plugins, what holds them together?

This article starts with a hypothetical filesystem backend replacement, rather than model performance. We follow how Cordis uses services, dependencies, and effects with cleanup to support that composition.

The source baseline is the August 13 snapshot 9fcdb2c. The official npm package @deepseek-ai/dsh@0.1.0-rc.6 was published that day, but has no gitHead; this article does not claim an exact correspondence between its complete build and that snapshot. The seven Cordis core files examined here match the source shipped in @deepseek-ai/cordis@4.0.1, also published that day. The experiment runs that pinned package. Historical source · Official package metadata

Separate reading a file from reading it locally

A file tool often mixes several responsibilities: explaining parameters to the model, validating paths and input, calling the operating system, and formatting the result. When those responsibilities are tightly coupled, a new backend can require a rewritten tool.

Harness divides a capability into three roles. A Service Definition specifies the filesystem interface, a Provider implements it, and a Consumer uses it. These are responsibilities in the code, not three new names for the same layer. Capability architecture

For file operations, FileSystem extends Cordis Service and registers the capability as ctx.fs through super(ctx, 'fs'). LocalFileSystem implements local behavior. The model-facing tool-fs package owns tool parameters, read windows, formatting, and related policy cooperation. It depends on ctx.fs rather than hard-coding the local provider. Filesystem definition · Local implementation · Tool consumer

The replacement now has a defined target: preserve the interface required by the consumer and replace its provider. The tool need not acquire a chain of conditions for remote addresses, or know whether the implementation uses local system calls or a remote protocol.

Matching an interface still requires matching its semantics. Target resolution in the filesystem definition is asynchronous; its documentation explicitly accommodates a remote backend needing a round trip to establish a stable target identity. Path normalization, writes, cancellation, and permissions must satisfy the interface. Giving a new class a read method is far from sufficient.

The benefit is concentrating change in a place that can be reviewed. The cost is defining the interface carefully. Otherwise, the backend appears replaceable while execution still depends on hidden assumptions that every file is local.

File tools depend on a filesystem definition through ctx.fs, with local or alternative providers implementing it
Stabilize responsibilities before replacing implementations: tools handle model interaction, interfaces specify operation semantics, and providers perform the work.

Declare what you need instead of who starts first

An interface explains how to call a capability. What determines startup order? If the file tool loads before the filesystem arrives, should it fail immediately or implement its own retry loop?

The tool-fs dependency declaration names tools, fs, and systemPrompt. Cordis decides when the plugin can run from the availability of those services. The tool does not need to wait for a global startup event or guess whether a provider has finished construction.

A plugin can also establish a smaller dependency scope inside itself. The filesystem tool uses ctx.inject(['attachments'], ...) to register image-reading functionality separately. Once the base dependencies are ready, basic file tools can be established. The image capability is added when attachment storage becomes available. Without a service that can durably store image data, read_image is not advertised first only to fail later. Conditional capability assembly

The central runtime object is called a Fiber: an instance of a plugin. It tracks dependencies, configuration, lifecycle state, and cleanup functions. Here, Fiber does not mean an operating-system thread or a separate process. Plugin instance implementation

We ran a small experiment against the real Cordis package: mount a consumer requiring storage, then mount a simulated provider. The consumer did not execute before the service arrived. Once it appeared, the consumer started and installed a listener. Storage is a test name, not a built-in Harness service, and no remote files were accessed.

This expresses more than a hand-written instruction to load A before B. Dependencies become runtime data. The system can reassess them when services change, rather than walking a startup list once and forgetting it.

When the provider changes, consumers must notice

Allowing a dependency to arrive late solves only half the problem. The harder case is a consumer using A when A is unloaded and B later replaces it. How do you prevent an old listener from retaining A while new logic starts using B?

Cordis’s reflection layer notifies relevant plugin instances when a service changes. The implementation traverses registered runtimes and their Fibers, checks whether their dependencies contain a changed service and whether their scopes match, then rechecks the provider and refreshes state. Service-change notification

A Fiber does more than record a boolean saying all dependencies exist. During refresh, it constructs an epoch from the UIDs of the Fibers providing its dependencies. If any required service is absent, it uses an inactive marker. Consequently, changing the provider instance changes dependency identity even when the service name stays the same. Dependency epochs and transitions

An epoch change drives unloading or loading. When lifecycle work is already underway, the new change is recorded and checked after that work finishes. Loading also checks the epoch at an asynchronous checkpoint, avoiding execution of plugin code for an assembly attempt that is already stale.

In the experiment, removing the simulated provider caused the consumer to release its listener. Providing another instance activated the consumer again, with event handling using the replacement. Explicitly disposing the consumer was different: later service arrivals did not bring it back. Temporarily missing a dependency and deliberately destroying a plugin are distinct states.

There is an algorithmic cost. The current notify path scans plugin instances and checks the names changed in that notification. Its worst-case traversal can be approximated as instance count multiplied by changed-name count, plus dependency refresh work for affected instances. This is not a constant-time lookup from a service name to every consumer. Whether that scan matters depends on real plugin counts and change frequency; the structure alone is not a performance benchmark.

A consumer waits for dependencies, activates on provider arrival, then cleans up and rebuilds when the provider changes
Changing dependency identity drives reassembly. It does not automatically transfer an old task’s progress to a new backend.

The same service name can belong to different rooms

Putting every service in one global object would still let a replacement affect the entire Agent system. What if one conversation should use a simulated filesystem while another continues using the normal project?

A Cordis Context is more than a dictionary. Service lookup passes through a proxy and reflection layer. isolate('fs') creates a child Context with a new scope label, separating that branch’s fs resolution from its parent. Names not separately isolated retain their inherited relationships. Two isolation calls can explicitly use the same label to join the scope of that service. Context isolation

The same tool code can therefore resolve different providers from different Contexts. The experiment verified that a simulated service in a child scope did not change the parent’s service, and that matching labels allowed service sharing.

The name isolate does not make this a security sandbox. It separates dependency-resolution namespaces, not memory, filesystem, or network permissions. Plugins still execute in their host environment. Treating service scope as a security boundary would confuse it with the execution isolation discussed in the previous series.

Everything mounted needs a way to come down

Reassembly only works if the old capabilities can be withdrawn cleanly. Otherwise, every provider replacement leaves another listener, tool entry, or timer behind.

Cordis models these registrations as effects: establish an effect and provide its cleanup function. Framework operations such as ctx.on and service provision participate in the corresponding lifecycle. When a plugin unloads, the host can follow recorded ownership instead of guessing what the plugin changed. Effect implementation · Service registration and removal

Cleanup order deserves a close reading. Cleanup functions collected by a single effect execute in reverse order, awaiting asynchronous cleanup when needed. But a Fiber unloads multiple top-level effects with Promise.all. It would be inaccurate to say that all resources are closed in one globally serial reverse order. If connection B must close before A, keep that ordering inside one effect or orchestrate it explicitly.

The experiment verified that two cleanup steps within one effect completed in reverse, awaited order, and that repeated disposal did not execute the same cleanup group again. It does not cover every path where a disposer throws, and it does not mean the host can discover resources a plugin never registered.

Cancelling a model request, stopping a file write in progress, and handing an old task to a new provider remain responsibilities of application and backend protocols. Dependency reassembly restores consistency in the capability structure. It cannot reverse external side effects that have already happened.

When applying this design to another Agent platform, I would start with one small capability. Preserve the tool’s inputs and outputs, supply two simulated providers, repeatedly mount, remove, and restore them, and observe listener and resource counts. That tests whether the interface is independent and exposes state still retained by old closures after unloading. Proving that one component can be replaced completely is more useful than renaming every module a plugin from the outset.

Make composition something people can explain

Harness boot configuration follows the same idea. A profile stacks bundles, then applies user and additional overlays to produce the plugin tree. The architecture documentation describes patches that identify a row by ID and replace its whole configuration, rather than promising recursive merging of arbitrary nested fields. Configuration needs explicit replacement semantics to match replaceable runtime capabilities. Configuration assembly

This article ran 19 checks against the actual Cordis runtime, using simulated services to verify dependency waiting, withdrawal and restoration, scope isolation, listener removal, and effect cleanup. It did not start the complete dsh CLI, call a model, or migrate a real project to a remote backend.

Whether replacing an Agent component is straightforward depends on whether its interfaces, dependencies, and exit behavior were defined beforehand. DeepSeek Harness makes those assembly relationships readable, testable, and replaceable alongside the model-facing capabilities. The next article follows tool execution further, examining how those capabilities become control flow that completes a task.

继续阅读