← Writing

DeepSeek Harness

When One Agent Needs Help, Who Organizes the Others?

Follow a repository review to see how DeepSeek Harness orchestrates child Agents with JavaScript: concurrency slots, pipelines, partial failure, and cleanup after cancellation.

Imagine inheriting an unfamiliar repository and wanting a first architectural map. Authentication, storage, the job queue, and the frontend entry points each tell part of the story. You want someone to read them separately, then bring the findings together.

At first, it looks like an ordinary conversation. Ask an Agent to inspect authentication, wait for its answer, then move on to storage. After a few modules, something feels wrong. The main conversation is accumulating intermediate material. Its Agent is reading code, remembering unfinished assignments, and deciding whom to call next. Understanding the architecture and organizing the work have become tangled together.

A natural question follows: could the model write a small program first, leaving the known assignments, waits, and aggregation to code?

That is the problem DeepSeek Harness addresses with workflow. The repository review here is hypothetical, not an official case study or performance test. The source is pinned to the commit behind dsh-v0.1.1-rc.2. We are examining its subagent orchestration, not treating it as a universal execution system for arbitrary tools. Pinned engine source

Write down the assignments first

The previous article followed service interfaces and plugin composition. This time, a concrete consumer provides the entry point: tool-workflow exposes a model-facing workflow tool with script, meta, and optional args. Identity and description live in separate JSON data. The script is an ordinary JavaScript function body, supports top-level await, and ends by returning a JSON value.

That separation has a practical purpose. Names and descriptions need not pass through code evaluation, and the script does not have to double as a configuration file. Before publishing a run, the engine validates metadata, parses the body, resolves a registered subagent provider, and checks the requested total-child allowance. A request that cannot begin fails immediately rather than producing a misleading running record. Tool entry point, startup validation

Our illustrative script could look like this, with args.modules supplied by the caller. It shows the control flow; a short prompt alone does not guarantee a trustworthy architectural assessment.

phase("Read modules");
const notes = await parallel(
  args.modules.map(name => () => agent(
    `Read the ${name} module: identify entry points, dependencies, and open questions`,
    { label: name }
  ))
);
return args.modules.map((name, i) => ({
  module: name,
  status: notes[i] === null ? "failed" : "completed",
  notes: notes[i]
}));

The main Agent submits one orchestration request. The script can await several children and return only its chosen final result. This reduces the need for the main conversation to shuttle intermediate material around, but it does not eliminate child model calls or guarantee lower token costs. This version also registers explicit usage guidance with the tool: use it only when the user asks for a workflow or large multi-agent orchestration; prefer ordinary subagent calls for one or two delegations. Usage guidance and result wrapper

What moves into the worker?

It is easy to picture workflow as moving every Agent into another thread. What actually moves is the orchestration script. Each run creates a Node worker, and its script executes inside a vm context. The real child Agents still start on the host through ctx.subagents.

When the script calls agent(), the worker sends a start message. The host asks the provider for a child run, then sends back identity and result messages. The script can await a result like a normal Promise while Agent lifecycle ownership remains within the Harness service system. Host bridge

The workflow script runs in a worker while the host starts child Agents through its provider
The worker owns orchestration code; the host owns child startup, results, and cleanup.

The split addresses a concrete failure: a synchronous loop in a script should not freeze the main Harness event loop. The host can also terminate the worker. It is not a security sandbox, however. The project explicitly documents that node:vm is escapable and that escaped code can reach Node capabilities under the host process's authority. The absence of deliberately injected filesystem or network APIs does not prove security isolation. A deployment executing genuinely untrusted scripts needs a stronger implementation behind the same WorkflowEngine interface. Trust model

Queuing together is not starting together

Back to the review: suppose there are twenty modules. Does parallel() launch twenty Agents at once?

The implementation separates two operations. parallel() validates its input and item count, invokes each zero-argument function asynchronously, and collects results with Promise.all. The actual child concurrency limit lives inside agent(): a free slot is acquired immediately; otherwise the caller joins a FIFO queue. A call releases its slot and wakes the next waiter only after disposing its child run. Runtime scheduling

The limit therefore applies to active agent() calls, not every JavaScript function. Pure computation, ordinary Promises, and other work created by the script do not automatically enter a universal task pool. Even when a short description calls this bounded parallelism, that distinction matters.

Three settings measure different things. maxConcurrentAgents limits occupied slots; its default zero resolves to min(16, max(1, availableParallelism() - 2)). maxTotalAgents, defaulting to 1000, limits the call allowance accepted by one run. maxItemsPerCall, defaulting to 4096, limits the size of a single combinator input. Deriving a default from CPU parallelism says nothing about the model service's quota or optimal throughput. Configuration and resolution

The total counter increments before waiting for a slot. Twenty calls can consume twenty allowances while only a few have reached host-side startup. The normal agentsStarted result is therefore not a count of successful completions. If the worker is forcibly terminated, the host can report only the start requests it observed, which gives that path a different counting scope. Cost accounting should use actual model usage rather than multiplying this field by an assumed price. Counter placement, termination limitations

A fast module need not wait for a slow one

Now suppose each module note needs a second review. The obvious schedule reads everything first and reviews everything second. But authentication may be quick while storage requires tracing a lengthy transaction path. The authentication note waits simply because it belongs to the same batch.

pipeline(items, ...stages) chooses a different schedule. Each item has its own chain: when its first stage finishes, its second stage can begin. There is no barrier between stages across different items. The implementation still uses Promise.all to keep the final array aligned with input order, but array order is not completion order. Pipeline implementation

Items advance through their own stage chains; ordinary failures and fatal errors follow different paths
Dependencies belong to each item's chain; local failures and run-level faults propagate differently.

This fits a read-then-review process where each module can be reviewed independently. If review requires every module's first-pass findings, explicitly await the first batch before starting the next. Calling phase("Review") does not create a barrier. It only updates the display label used by later calls and notifies observers. Concurrent branches also share that current label. For precise attribution, pass an explicit phase to agent() rather than confusing a visual grouping with an execution dependency. Phases and labels

Should one failed module stop the batch?

If a child Agent resolves with an ordinary failure status, agent() returns null to the script. If a task function throws an ordinary exception inside parallel(), the combinator likewise substitutes null for that item and preserves the others. A repository survey can return which modules succeeded and which need another look, instead of discarding every finding because one corner failed.

Not every error can be absorbed this way. Invalid arguments, unsupported options, exceeded item or call limits, provider startup failures, and unexpectedly rejected child-result Promises follow the fatal WorkflowError path. Combinators rethrow these errors; if the script leaves them unhandled, the run fails. A child failing at its assignment and broken execution infrastructure should not disappear into the same empty value. Error taxonomy, catch sites

There is another subtle distinction. In pipeline(), a stage that throws an ordinary exception skips that item's remaining stages. A stage that returns null still passes the value to the next stage. An ordinary child failure produces precisely the latter behavior. The review stage should therefore check explicitly for a preceding null and decide whether to return a failure record or continue. Avoid casually using .filter(Boolean) too: valid stage outputs of zero, an empty string, or false would be removed as well.

A fatal rejection also does not make JavaScript's Promise.all cancel its other branches automatically. The runtime chooses the outcome. On settlement, the host closes admission, cancels pending starts, and begins disposing published children. Error propagation and resource cleanup are separate mechanisms that work together. Host settlement handling

Who is still running after Stop?

For a published run, run.result fulfills with an outcome object. Its stopReason distinguishes completion, error, and cancellation; these outcomes are not represented by Promise rejection. The tool translates a non-completed outcome into a model-visible tool error and awaits run.dispose() in finally.

Cancellation does not rely entirely on cooperation from the script. The host records the first cancellation reason, notifies the worker, and aborts the signal shared by pending starts and published children. The worker rejects queued callers, and subsequent hook calls report cancellation. If the script still does not settle, the host waits for the configured grace period, force-settles the run, and terminates its thread. Tool cleanup, host cancellation

The default syncTimeoutMs is a VM timeout for the initial synchronous slice, not a total deadline for the asynchronous script. disposeGraceMs bounds waiting during cancellation and disposal. Both default to 5000 milliseconds, but their responsibilities differ. Terminating the worker does not roll back external side effects either. Whether a remote operation can stop still depends on how its subagent provider and tools respond to cancellation.

Finally, the returned value passes through a plain-JSON check. Cycles, non-finite numbers, functions, and nested undefined, among other unsupported values, are rejected instead of silently changing meaning at the thread boundary. A missing return value is converted to null by the runtime. The main Agent receives the selected final value, not every intermediate child context. Value materialization

Returning to our opening task, the final report should retain module names, success or failure status, and unresolved questions. A polished summary alone can hide failed items. Dumping every child response back into the main conversation gives up the opportunity to curate the result. The script can choose what to return, but it also needs to preserve the evidence that matters. An orchestrator can define array alignment and error propagation; it cannot establish whether an architectural explanation matches the repository.

Separate organizing work from understanding it

The useful lesson is not that calling more Agents makes a system smarter. The model still needs a sensible division of work, children still need to understand the code, and findings still need verification. Workflow takes over control logic that can already be stated explicitly: which tasks are independent, which results depend on others, whether a failure can remain local, and when execution must stop.

To check the key branches, I transformed and executed the pinned runtime.ts and realm.ts with controlled child-run substitutes. Fifteen checks covered concurrency slots, ordering, partial failure, stage advancement, limits, and JSON return values. This is source-level behavioral verification, not a full Harness run, a real-model experiment, or an end-to-end test of host-worker cancellation. The discussion of thread exit and provider cleanup is supported by source inspection.

Once the batch finishes, another question appears: after closing the page, which facts can be recovered? The next article follows Session events and how completed work becomes recoverable, forkable context.

继续阅读