~/satyajit

nac: an orchestrator that is not allowed to touch anything

mdjsonmcp

2026-08-14 · 8 min · agents · harness · open-source · rust · context · explainer

nac is Arcee AI's open-source agent harness, Apache 2.0, about 98,000 lines of Rust across three crates. The write-up opens with a diagnosis rather than a feature list:

We think this couples two things that should be separate: the temporary context needed to perform an action; and the persistent state needed to continue a workstream.

That is the whole design, and everything else follows from taking it literally.

The orchestrator cannot do anything

nac uses a thread-and-episode architecture adapted from Random Labs' Slate. A central orchestrator plans, decomposes, and decides what happens next. It has exactly one action available: launch threads.

Importantly, the orchestrator's only action is launching threads; it cannot execute commands or edit files on its own.

The blog states the split as two lines of pseudocode, which is the clearest thing in it:

orchestrator: decide and route, but do not act
workers:      act, but do not expand the orchestration graph

Each dispatch starts a worker — a fresh process with a fresh model context, given the worker system prompt, the requested action, its tools, and any applicable skills. The worker calls the model and uses tools until the model returns a response with no tool calls. That response is the episode.

There is no separate summarization pass. The worker's system prompt tells it that its final answer should be a concise handoff, so the answer is the summary. That saves a model call and, more importantly, means nothing gets summarized twice.

What survives

what a finished worker leaves behindno summarization pass
worker execution contextdiscarded
Every tool call, every file it read, every retry, every dead end. Discarded when the worker returns and never used as model context again. This is the whole point: the expensive part of doing the work is also the part least worth carrying forward.
the gap between the two kinds of persistence
Because environment changes persist and episodes only persist on success, a worker that edits files and then dies leaves the world ahead of the record. Arcee says so directly: worker failures are “not transactional,” and a returned error means “the environment may have moved ahead of persistent history.” Every system that separates durable state from a scratch context has this seam somewhere; it is worth more that they printed it than that they solved it.

The design is one sentence: separate the temporary context needed to perform an action from the persistent state needed to continue a workstream. Most harnesses put both in the same transcript and then fight the consequences with compaction. nac never merges them, so there is nothing to compact — the orchestrator only ever reads episodes, and the transcripts that produced them are gone by the time it plans again.

Once the episode exists:

the worker's execution context is discarded and never used as model context by the system again. Its changes to the environment remain, but the episode is the persistent representation of the work.

A thread is just a named, ordered list of episodes. When the orchestrator assigns that thread more work, a new worker starts fresh with the thread's accumulated episodes — never the transcripts that produced them.

This is why nac has no compaction problem. Compaction exists to compress a transcript that has grown too long; nac never lets the transcript reach the orchestrator in the first place. The orchestrator reads episodes, and by the time it plans again the execution details are already gone. It is a different answer to context rot than compressing history: don't accumulate it.

Thread weaving is the other half. A dispatch can name source threads, and nac resolves each to its most recent retained episode and hands it to the worker as context — but those source episodes never join the target thread's history. Only the new episode does. Threads stay their own.

A batch is a graph

one dispatch batch = one DAG
profile
Three same-batch sources, so this is a dependency edge three times over: it waits for all three, then receives their episodes as context. Their execution transcripts are already gone.

The orchestrator ends its turn by emitting a batch of these, and the batch is the unit of scheduling: nac builds the graph, rejects it if it is cyclic or names a target twice, runs what can run in parallel, and only returns control once everything has settled. That gives planning a clean synchronization point — the orchestrator never polls background work, and never sees a half-finished world.

The orchestrator ends a turn by emitting a batch of thread calls, each with a name, a free-form action, and optional threads, skills and timeout. Naming a source that is dispatched in the same batch creates a dependency edge; naming one that already finished just supplies context.

That makes the batch a DAG. nac rejects duplicate targets, validates acyclicity before executing, runs independent workers concurrently, and waits for the whole batch before letting the orchestrator plan again. The synchronization point is deliberate — the orchestrator never polls background work and never observes a half-finished world.

One place the code is more precise than the prose. The blog says a cyclic batch is rejected; crates/nac-core/src/agent/tool_exec.rs shows what actually happens on DagError::Cycle or DagError::DuplicateName: every thread dispatch gets an error result, while non-thread tool calls made in the same turn still execute normally. If your orchestrator mixes a query with a dispatch, that distinction matters.

The seam they printed

The honest part of this release is a sentence most teams would have left out:

Worker failures are not transactional: if a worker changes the environment and then exits before committing its final response, those changes may remain without a new episode, so a returned error means the environment may have moved ahead of persistent history.

Episodes persist only on success. Environment changes persist unconditionally, and live outside nac's state entirely. So a worker that edits files and then dies leaves the world ahead of the record, and nothing in the runtime knows.

Every system that separates durable state from a scratch context has this seam somewhere. What is unusual is printing it in the launch post rather than leaving it to be discovered.

Harnesses as inference runtimes

The framing section is the part I expect to get quoted, and I think it earns it. Arcee traces harness evolution along two axes — enriching context so each model call gets denser information, and expanding the action space so the model can initiate more capable operations — from tool use through program execution, memory, multi-agent search, Recursive Language Models, fresh-session harnesses, and finally Slate-style dispatch.

Then the claim:

An agent inference runtime constructs context, schedules inference, executes effects, preserves state, enforces capabilities, and defines how work synchronizes, fails, resumes, and stops. A thin harness executes a model-tool loop. A runtime owns semantics that would otherwise exist only implicitly in its transcript.

And the mapping, which is what makes it concrete rather than a slogan:

worker invocation  = inference operation
thread             = persistent program state
episode            = committed workstream update
source thread      = data dependency
dispatch batch     = dynamic execution graph

Their summary line is the one worth keeping: "judgment stays in tokens, invariants live in the runtime."

It is worth reading this next to DeepSeek Harness, which arrives at a related conclusion from the opposite direction. dsh keeps one agent loop and makes the log the authority, with a runtime invariant that refuses any request the log cannot reconstruct. nac keeps no shared log at all and makes episodes the authority, with a scheduler that refuses any batch it cannot order. Both are saying the harness should own guarantees the transcript used to own implicitly; they disagree about whether the transcript should exist.

Arcee also names two systems that make different choices — Onyx, which pushes orchestration control flow into persisted typed programs, and LongHorizon-Harness, which advances one globally audited task record through serial manager/executor/auditor rounds instead of parallel workstreams. Citing your neighbours accurately is a good sign.

When it is the wrong tool

Stated plainly, which is rarer:

For a single focused change that fits in one coding-agent session, going direct is simpler and often faster. That adds overhead because the orchestrator cannot perform the task itself; it still has to delegate to a thread.

The architectural purity has a fixed cost: a one-line fix still requires a dispatch. Their stated fit is work with a meaningful high-level objective, hard boundaries stated up front, a concrete definition of done, enough independent work to justify parallelism, and freedom for nac to choose its own decomposition — reproducing an ML paper, porting a large codebase, decomposed code review, large parallel change jobs on a dedicated branch and worktree.

The meta-orchestrator pattern

nac ships an MCP server, so Claude Code or Codex can dispatch, monitor and steer nac jobs as tools. Arcee's preferred pattern is to make the interactive agent a meta-orchestrator: it works with you in a normal session, watches for work that is decomposable with a concrete definition of done, writes the job description itself, and hands it to nac to run in the background.

The capability boundary is drawn carefully:

Through nac's MCP interface, the meta-orchestrator still cannot see a worker's discarded execution context or the underlying environment directly; the MCP server exposes no file or shell tools of its own.

So the outer agent gets the same view a human gets — orchestrator chat, thread episodes, recent events, the ability to steer — and no more. State must be queried; it is not pushed into the meta-orchestrator's context. The restriction that defines the inner orchestrator is applied to the outer one too.

What is missing

No evaluation. No benchmark, no comparison against a single-agent baseline, no measurement of the token savings the architecture is supposed to produce. For a design whose central claim is that separating temporary from persistent context makes long tasks work better, there is no number showing it does. The evidence offered is a timelapse video and the fact that Arcee uses it internally.

No cost accounting. Running an orchestrator plus N parallel workers, each with its own context, is not obviously cheaper than one long session — it trades context length for context count. Which way that lands is exactly the thing an evaluation would tell you.

The repository is six commits old at the time of writing. This is a design worth taking seriously and a codebase worth waiting on.

What I'd take from it

The transferable idea is the prohibition, not the architecture. Most multi-agent systems let the orchestrator do a little work itself when delegation feels heavy — and that is precisely when the orchestrator's context starts filling with execution detail and the original intent starts getting diluted. nac removes the option. The orchestrator cannot act, so its context stays a plan.

That is a constraint you could impose on a system you already have, without adopting threads, episodes, or Rust.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "nac: an orchestrator that is not allowed to touch anything", ai.thesatyajit.com, August 2026.

bibtex
@misc{ghana2026nac,
  author = {Satyajit Ghana},
  title  = {nac: an orchestrator that is not allowed to touch anything},
  url    = {https://ai.thesatyajit.com/articles/nac},
  year   = {2026}
}
share