# nac: an orchestrator that is not allowed to touch anything

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/nac
> date: 2026-08-14
> tags: agents, harness, open-source, rust, context, explainer
[nac](https://github.com/arcee-ai/nac) is Arcee AI's open-source agent harness, Apache 2.0, about 98,000 lines of Rust across three crates. The [write-up](https://arcee.ai/blog/nac) 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](https://randomlabs.ai/blog/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:

```text
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

<ContextLifecycle />

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

<DispatchDag />

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](/articles/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:

```text
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](/articles/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.
