How Cordis Turns a Plugin List into a Live Agent

DeepSeek Harness uses Cordis to answer a practical systems question: how can independent plugins provide services, coordinate decisions, and disappear cleanly inside one agent runtime? This chapter focuses on the mechanisms behind that answer—context services, injected readiness, typed events, scoped registrations, and effect-based teardown—so readers can predict when a Harness plugin is usable and what happens when it is removed.

A configuration file may look like a list of names, but the running system is not a list. It is a dependency graph with lifecycles. A model-facing tool needs a tool registry; the registry needs prompt assembly; an agent loop needs the model and the tools; a UI needs live agent events and durable session events. Cordis turns those relationships into runtime rules rather than leaving them as assumptions for each package author.

A context is a live service directory

In Cordis, a context is a place where plugins publish and consume services. A service is not just a utility function. It represents a capability with a stable key, such as ctx.tools, ctx.llm, or ctx.sessions, and a public contract that consumers can use without knowing which implementation currently supplies it.

Consider a tool that should read a file. The tool itself needs to expose a name, description, argument schema, and result to the model. It should not also have to decide which filesystem implementation is active, how a path is represented, or whether the current deployment uses a local or remote execution world. Those decisions belong to services and providers around the tool. The tool asks the context for the capability it needs.

This is dependency inversion in an unusually direct form. A consumer depends on the meaning of a service; a provider depends on the same definition; neither needs a direct import relationship with the other. In DeepSeek Harness, that distinction is what allows a model-facing shell tool to use a local executor in one profile and a sandboxed or platform-specific executor in another composition.

The key is stable responsibility, not a promise that all implementations behave identically. The service definition sets the vocabulary and obligations. A provider may add platform-specific failure facts or capability limits, but it must still satisfy the definition. The consumer can remain unchanged only to the extent that the replacement respects the shared contract.

inject turns dependency documentation into readiness

A plugin declares required services through inject. If a plugin needs the tool registry, it declares that requirement instead of assuming another row happened to load first. Cordis then keeps the plugin pending until the required service is available.

This changes what “startup order” means. In a hand-written application, an author might call initializeTools() before constructing the agent and rely on a sequence of function calls. In DeepSeek Harness, the relationship is attached to the plugin itself. The tool plugin becomes active when its required service exists, and its registration occurs in that service’s context.

The dependency can be more than a convenience. The Harness tool registry contributes tool schemas to system-prompt assembly. A tool plugin that activates without the registry could appear to work locally while failing to make its schema visible to the model, or while bypassing the guarded execution path. Making the dependency executable prevents that partial activation.

The same idea applies to a provider. A plugin that consumes ctx.llm should not quietly substitute a default adapter when the requested service is absent. Missing capabilities should remain visible as load or configuration failures. This is important in an agent runtime because a silent fallback can change what the model can do, which credentials it uses, or which files it can reach.

The context is also where provider and consumer meet without becoming one package. A service provider publishes a stable service; the consumer looks it up through the context; Cordis resolves the dependency and owns the activation relationship. The result is a runtime graph in which the project can replace a provider by changing composition, while the consumer continues to speak the same service vocabulary.

flowchart TD P["Service provider"] -->|publishes| C["Cordis context"] D["Consumer plugin"] -->|inject declares requirement| C D -.->|missing service| W["pending until ready"] C -->|ctx.tools lookup| R["stable service API"] D -->|register through effect| R R --> E["owned registration"] E --> X["typed event dispatch"] X -->|waterfall next| N["downstream listeners"] X -->|listener returns| S["short-circuit decision"] A["agent-scoped context"] -.->|visibility + lifetime| E U["plugin unload"] --> V["effect disposer"] V --> E

The diagram captures two different relationships. inject connects a consumer to the context’s readiness, while ctx lookup connects the active plugin to a service contract. After activation, registrations become effects and events become communication paths. A scope can narrow visibility, but it does not turn the context into a process boundary.

Events carry control semantics, not just notifications

Services are useful for direct capability calls. Events are useful when several plugins need to observe or influence a lifecycle without importing one another. DeepSeek Harness uses typed events for both durable observation and live interception, but the dispatch mode determines what a listener is allowed to mean.

An emit event is a notification. It has no return value and is not a decision point. The session/event feed uses this kind of channel for post-commit observation: persistence, projections, telemetry, and other consumers can learn that a Session log grew. The append has already been accepted; an observer does not get to rewrite it.

A parallel event awaits all listeners, which suits a checkpoint where every durability participant must settle. A serial event runs listeners in registration order and awaits them, which suits an ordered terminal checkpoint. A bail event stops when a listener returns a meaningful result, which suits a first owner of a decision.

A waterfall is different from all three. It is an around-middleware pipeline. A listener receives the current inputs and a next() function. Calling next() delegates to the downstream listeners and gives the current listener a chance to inspect or transform their result. Returning without calling next() intentionally short-circuits the chain.

That rule is central to Harness policy. Suppose a tool call reaches tools/pre-execute. A policy plugin can decide that the call is allowed and delegate, ask for approval, or deny it. If the plugin only annotates the call, it must call next(); if it owns the decision, it returns. A listener that forgets next() is not merely missing a callback. It has changed the runtime’s control flow by stopping the chain.

The same pattern appears before a model step. agent/pre-step can accept, rewrite, or reject the messages proposed for the step. agent/request can participate in request construction. agent/request-error can claim a failed model request and return a retry decision. These events let features such as hooks, compaction, plan mode, and model-specific recovery attach to the public Agent lifecycle without editing the default loop.

Typed declaration merging keeps those extension points legible to TypeScript. A plugin adds an event name and its payload contract to the shared event map; the dispatch call and listener then use the same type. The project also records the dispatch mode as part of the event contract. That prevents a consumer from treating an asynchronous checkpoint like a fire-and-forget notification or treating an interception point like a passive log.

The distinction between Cordis events and durable Session events is equally important. agent/pre-step is a live Cordis event. turn/start and tool/result are durable event types carried through the Session log. A plugin observes those durable facts through session/event and inspects the event type. The similar naming reflects related domains, not one shared dispatch mechanism.

One agent can have a different runtime composition

A global service is not enough for a product with multiple agents. In the Web profile, two sessions may need different prompt sections, tools, restrictions, or delegation behavior while sharing one process. DeepSeek Harness handles that through scoped contexts.

A scope is a registration world associated with an opaque key. A contribution made through an agent’s scoped context is both visible to that agent and owned by that agent’s lifetime. A scoped tool can shadow a global tool with the same name for that agent. A scoped prompt section or variable can replace the global version for one session. A scoped event listener can receive activity for its subject without receiving unrelated agents’ events.

Visibility follows the scope chain. A child scope can inherit an ancestor’s registrations, with the nearest definition taking precedence. Event admission works in the opposite direction: an ancestor listener can observe activity dispatched for a descendant when the event is scope-filtered. This lets the system express relationships such as a parent coordinating a child while keeping sibling agents separate.

The Web profile uses this capability through agent presets. Instead of mounting the same model-facing tool rows globally for every browser session, the host can compose each session’s agent from a preset. One session may receive a particular tool set or persona while another uses a different composition. The host-level services that must coordinate across sessions can remain global, while agent-facing registrations live in the session scope.

This is registration isolation, not authority isolation. The scope primitive routes trusted same-process plugins. It does not block a plugin from using a service it already receives, and it does not make an untrusted plugin harmless. An arbitrary service remains context-global unless its API is designed to be scope-aware. Lineage facts such as a parent session or delegation depth are also data; they do not create a security boundary through the scope hierarchy.

That boundary matters when choosing between mechanisms. A scope is appropriate when one agent needs a different tool roster, prompt, or listener set at low coordination cost. A sandbox is needed when a process’s file effects must be constrained. A separate process or remote executor is needed when the same host world is not an acceptable trust boundary. Cordis gives the application a way to compose those stronger capabilities; it does not supply them merely by creating a scope.

Effects make replacement and teardown part of behavior

A registration is an effect with an owner. When a plugin calls ctx.on() to listen for tool results, the listener belongs to that plugin’s fiber. When a tool registers with ctx.tools, the registration belongs to the same lifetime. When a provider publishes a route, its disposal removes that route. The system does not need every feature author to remember a separate cleanup list.

This is especially valuable during hot replacement. A live configuration edit can unload one plugin and load a new instance. If the old listener remained, both versions might observe or transform the same request. If the old tool remained, the model-facing schema could advertise a capability whose implementation no longer matches the current configuration. Effect ownership makes replacement a remove-then-add operation rather than an accumulation of hidden state.

The ownership rule also reveals why lifecycle is part of a service contract. A provider that starts a process must define how its process is stopped. A provider that owns a session write handle must drain it before release. An agent handle must stop and await the loop before its scoped registrations disappear. DeepSeek Harness’s default Agent creation path makes setup and publication a rollback-covered transaction: scoped behavior is installed before announcement, and a failure before publication can remove the unpublished resources without exposing a half-created agent.

Disposal is not merely a request to stop. The project’s defensive rules require cleanup to reach quiescence. A teardown that sends an abort and returns immediately can leave a child process, callback, or notification running against an object that has supposedly gone away. For that reason, the Agent handle’s disposer stops and drains the loop, unwinds the scope, detaches the Agent, and then detaches the Session. Similar ownership rules apply to background processes, persistence handles, and remote subscriptions.

There are limits to what automatic effects can solve. A plugin can keep mutable state in a closure, and an asynchronous operation may need an explicit quiescence contract. A scoped context also carries the service-resolution reachability of the plugin that minted it; handing a context to another component does not retroactively narrow those dependencies. Effect-based cleanup is therefore a strong lifecycle discipline, not proof that every asynchronous or security property is automatically correct.

The runtime consequence

Cordis gives DeepSeek Harness a way to assemble an agent from contracts rather than from a fixed call graph. inject controls readiness, services provide stable capability names, event modes define observation and intervention, scopes vary one Agent’s registrations, and effects bind those registrations to teardown. The result is a live runtime whose behavior can be changed by composition while its ownership rules remain visible.

That foundation explains why the next question is about execution rather than loading. Once the services are available and the registrations are active, a user message still has to become model input, a streamed response, tool work, and a durable result. The next chapter follows that accepted message through the Agent’s turn and step lifecycle, where the plugin tree becomes an actual conversation.

References