DeepSeek Harness TypeScript deepseek-ai/deepseek-harness

The Mental Model: Why There Is No Core

Seven stops that turn the "everything is a plugin" claim into something you can point at, file by file

8 stops ~15 min Verified 2026-08-17
What you will learn
  • Why dsh has no privileged core and what that means for every change
  • What "plugin" means concretely: services, typed events, and reversible effects in a shared context
  • Where each core capability lives and the `ctx` key that exposes it
  • Why the agent loop itself is a plugin and how the rest of the system stays decoupled from it
  • How the CLI is just a dispatch mode that boots a tree of plugins
  • Why any row of the boot config can be replaced by a patch of your own
Prerequisites
  • You can read TypeScript and basic YAML
  • You do not need to know Cordis — this tour introduces it
1 / 8

There Is No Privileged Core

docs/architecture.md:9

The one sentence that changes how you read the whole codebase: there is no core to patch, you mount a plugin beside the others

Almost every agent framework has a center of gravity: an Agent class, a runner, a loop you configure around. dsh inverts that. The model adapter, the tool registry, the session log, and the agent loop are all at the same altitude. When you read a package asking "is this core or an extension?" the answer is always: it is a plugin. That is the mental load that drops first.

Key takeaway

"Everything is a plugin" is a structural rule, not a slogan. If a change feels like it needs a special case in a privileged core, you are looking at the wrong layer. ---

## Cordis

[Cordis](cordis-primer.md) is the framework under dsh: plugins contribute services, typed events, and reversible effects to a shared context. Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so every part is replaceable from configuration.

There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.

## Profiles and bundles

A running `dsh` is a plugin tree composed at boot from ordered layers.

A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web` and `headless` ship as templates.

A **bundle** is a distribution format for Cordis config rows and the code they mount, so whatever it inserts stays patchable by the layers above it.

Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile` lists a profile's bundles, and `dsh.bundle` points at a bundle's patch file.
2 / 8

What a Plugin Contributes

docs/architecture.md:78

A plugin's surface is services it exposes and events it can observe; events are the extension points and choosing the right domain is the first decision in most changes

Three event domains shape the code. Durable session events are the log. Live agent events let you observe or intercept work in flight. Capability events attach policy to a seam without importing the loop at all. A new feature is usually a listener on one of these, not a modification of an existing class.

Key takeaway

To find where new behavior goes, decide which domain a plugin lives in: log it, observe the agent, or attach to a seam. ---

     step/end
     tools owe another request, or next-step input arrived -> claim -> next step
  -> agent/turn-stopping
turn/end
```

`turn/*`, `step/*`, `user/message`, `assistant/*`, and `tool/*` are durable session events; the rest are live extension points across three domains. `agent/pre-step`, `agent/request`, `llm/stream`, and the three `tools/*` events are waterfalls, whose listeners must call `next()` to delegate; `agent/turn-stopping` is serial and has no `next()`.

Input reaches the driver through one inbox. Some messages wake it immediately; injected context waits in the inbox until another message does.

`agent/pre-step` decides what the model sees. Listeners may rewrite the claimed messages or reject them outright; a rejected or empty first claim still closes a durable turn that spent no step, so the log records the attempt. Each step reads the prompt sections and tool schemas that plugins registered.

Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-execution-pipeline.md), and [cancellation and error recovery](subsystems/core.md#the-agent-handle).
3 / 8

The Core Capability Map

docs/architecture.md:61

The small set of core packages and the shared-context (ctx) key each one owns

This table is the map. Each package owns a capability and exposes it under a ctx key. The loop is ctx.agentLoop; the model is ctx.llm; the tools are ctx.tools. Nothing imports a concrete driver — they depend on the key and the events, which is what keeps every one of them replaceable.

Key takeaway

Navigate by the ctx key, not by the package name. The key is the stable contract; the package behind it is swappable. ---

The [event map](event-producer-consumer.md) lists every event's producers and consumers.

## Turn flow

A **step** is one model request plus the tools it calls. A **turn** is zero or more steps: it opens before its first input is claimed and closes once nothing is owed.

```text
turn/start
  claim next-step input plus one queued message
  assemble prompt sections + tool schemas
  -> agent/pre-step                   reject | enter(messages)
     reject, or a first enter rewritten empty -> close the turn with no step
     step/start
     append entered messages as user/message
     derive model history from the log
     agent/request -> llm/stream -> assistant/chunk* -> assistant/message
     tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
     step/end
4 / 8

The Loop Is a Plugin Too

packages/core/agent-loop/src/agent.ts:63

ReactLoopAgent is the concrete default driver, the only package with real loop logic, exposed as ctx.agentLoop

Note the class _implements_ Agent (defined in core/agent). This driver is the default, not the only one. Because other packages consume the Agent interface and ctx.agentLoop, and because its registrations are reversible effects, a different loop could be mounted in its place. That is the claim in Stop 1 made provable.

Key takeaway

Find the loop by its interface and its ctx key. The concrete class you are looking at is replaceable, and the design is built to keep it that way. ---

/** Drives one session through turn and step boundaries. */
export class ReactLoopAgent implements Agent {
  readonly inbox: Inbox
  private phase: Phase
  private activityDone: Promise<void> = Promise.resolve()

  /** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
  readonly scope: Scope
  readonly ctx: Context

  /** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
  private readonly dispatch: AgentEventDispatch

  /** Whether this loop instance has appended its initial/resume request anchor. */
  private requestHeaderLogged = false
  private readonly runtimeContext: RuntimeContextProjection

  constructor(
    private loopCtx: Context,
    public readonly id: SessionId,
    public readonly options: AgentOptions,
    public readonly session: Session,
  ) {
    this.dispatch = agentEvents(loopCtx, this)
    this.inbox = new Inbox(session, {
      inserted: (message) => { this.dispatch.emit('agent/inbox/inserted', { message }) },
      discarded: (message) => { this.dispatch.emit('agent/inbox/discarded', { message }) },
      claimed: (message, turn) => { this.dispatch.emit('agent/inbox/claimed', { message, turn }) },
    })
    const lastTurn = session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0
    this.phase = { kind: 'idle', lastTurn }
    this.scope = createScope(loopCtx, this)
    this.ctx = this.scope.ctx.extend({ agent: this })
    this.runtimeContext = new RuntimeContextProjection(this.ctx, session)
  }
5 / 8

The CLI Is a Mount, Not an Engine

apps/cli/package.json:8

The dsh binary's "engine" is an almost-empty list of plugin packages it depends on

The CLI does not contain an agent. It depends on a long list of plugin packages — the base bundle, the app boot, the loop, the commands. The "product" is the sum of the tree it boots, not this file. Add a dependency, and you have added a feature; remove one, and it is gone.

Key takeaway

Capability is a function of the plugin tree. The entry point is just the thing that composes and mounts it. ---

  "repository": {
    "type": "git",
    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
    "directory": "apps/cli"
  },
  "type": "module",
  "bin": {
    "dsh": "lib/bin.js"
  },
  "files": [
    "lib/*.js",
    "config"
  ],
  "license": "MIT",
  "dependencies": {
    "@deepseek-ai/cordis-plugin-hmr": "workspace:^",
    "@deepseek-ai/cordis-plugin-include": "workspace:^",
    "@deepseek-ai/cordis-plugin-loader": "workspace:^",
    "@deepseek-ai/cordis-plugin-timer": "workspace:^",
    "@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
    "@deepseek-ai/dsh-app-boot": "workspace:^",
    "@deepseek-ai/dsh-base": "workspace:^",
    "@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
    "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
    "@deepseek-ai/dsh-client-ui-cordis": "workspace:^",
    "@deepseek-ai/dsh-command-compact": "workspace:^",
    "@deepseek-ai/dsh-command-goal": "workspace:^",
    "@deepseek-ai/dsh-compaction-basic": "workspace:^",
    "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
    "@deepseek-ai/dsh-goal": "workspace:^",
    "@deepseek-ai/dsh-goal-round-driver": "workspace:^",
    "@deepseek-ai/dsh-cmdline": "workspace:^",
    "@deepseek-ai/dsh-launch-environment": "workspace:^",
6 / 8

A Bundle Declares Itself

packages/bundle/base/package.json:36

How a bundle registers into the composition — a single dsh.bundle field pointing at its patch file

This is how a bundle announces its existence to the compositor. It points at a cordis.patch.yml that describes the config rows and code it mounts. The base bundle is the first layer of every profile; the web and headless bundles stack on top of it. Each is just package.json plus a patch — nothing privileged.

Key takeaway

A bundle is declarative. It contributes rows and code through a patch file; the compositor applies them in order. ---

  "dsh": {
    "bundle": {
      "patch": "./cordis.patch.yml"
    }
  },
7 / 8

Boot Composes the Tree

packages/boot/app-boot/src/index.ts:757

boot() builds a Context, mounts the root include, waits for the tree to load, and asserts every entry activated

Boot does not construct an agent. It makes a shared Context, installs a Loader, mounts the root config (the profile's composed tree), and waits until every entry in that tree is active. Failure is labelled by stage. This is the whole "boot": compose a plugin tree and wait for it to settle.

Key takeaway

Boot = build a Context + mount a plugin tree + assert it activated. There is no hidden core being built underneath. ---

export async function boot(
  binName: string,
  absoluteConfigPath: string,
  patches?: PatchOptions[],
  prepare?: (ctx: Context) => Promise<void> | void,
  bareModuleBaseUrl?: string,
): Promise<Context> {
  const ctx = new Context()
  // Two failure labels: `prepare` runs before any config-tree entry mounts,
  // so its failure is host setup, not the plugin tree.
  let stage = 'host preparation failed'
  try {
    ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/'
    ctx.provide('dshHomePath', dshHomePath)
    await ctx.plugin(Loader)
    await prepare?.(ctx)
    stage = 'plugin tree failed to load'
    await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl)
    // A surface can finish and dispose the whole tree while startup is still
    // in flight, before the last entry settles. The Loader service goes with
    // it, and the activation audit describes a live tree — reading `ctx.loader`
    // past this point would throw a TypeError over an app that exited exactly
    // as asked. Transactional group updates settle
    // lifecycle inside the mount, so the teardown can land before it returns;
    // re-check after every await.
    await ctx.get('loader')?.await()
    if (ctx.get('loader') === undefined) return ctx
    await assertEntriesActivated(ctx, binName)
    return ctx
  } catch (cause) {
    // Root-fiber disposal contains cleanup failures per observer (Cordis
    // fiber.ts hardening) and a repeated call returns the settled single-shot
    // result, so this await cannot reject and replace `cause`.
    await ctx.fiber.dispose()
    const detail = cause instanceof Error ? cause.message : String(cause)
    // The transactional Loader wraps a failing entry apply in one message per
    // tree layer; every layer's message is folded into `detail` above, and the
    // deepest cause is the plugin's own thrown error, whose stack names the
    // real failure site — append it so the startup diagnostic preserves the
    // original activation error instead of only the wrap chain.
    let deepest: unknown = cause
    while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause
    const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : ''
    throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause })
  }
}
8 / 8

Any Row Can Be Yours

docs/architecture.md:26

How a profile stacks bundles and patches, and that the dump prints the exact tree your machine boots — and any printed row can be replaced by a patch

This is the payoff of the mental model. Because the product is a composed tree of declarative rows, you can inspect the exact tree you booted and override any row without touching the package that owns it. Extending dsh and reading dsh are the same act.

Key takeaway

dsh --profile web --dump-config is the map of your system. Every row it shows is yours to replace. ---


Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows.

To see the tree your machine actually boots:

```sh
dsh --profile web --dump-config
```

Any row it prints can be replaced by a patch of your own.

Composition mechanics are in [app-boot](../packages/boot/app-boot/README.md#profiles); config fields are in the generated [config catalog](config-catalog.md).

## Core packages
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free