# Build your coding orchestrator from scratch part 1

> Source: <https://wiilearn.substack.com/p/spurcode-build-your-coding-archestrator>
> Published: 2026-10-24 03:13:58+00:00

Six months ago, I made up my mind to build **SpurCode**, a TUI coding agent orchestrator, from scratch.

At the time, the pain points I was facing on a daily basis mostly came from depending on a single coding agent. It was affecting code quality, productivity, and my ability to stay in a flow state because:

- Using the same model family to generate and review code seemed to result in lower-quality code reviews.
- Five-hour and weekly quotas kept breaking my flow state.
- Context management across multiple coding agents wasn’t effective at the time.

## **Orchestrator**

The idea behind an orchestrator is simple: you have a team of agents, and you can plan and assign specific tasks to different agents based on their profiles. For example, you might have an engineer agent, a product owner agent, and a sales agent. For each scope of work, you assign the task to a predefined agent.

For each agent profile, you can refine:

- The prompt (the agent’s profile prompt) and the specific agent.
- The model and the reasoning effort assigned to that agent.
- The tools the agent is allowed to use.

And yes, many coding agents already support agent orchestration, like Kiro, Claude Code, and Codex, where the main active agent can proactively delegate work to sub-agents.

But the coding agent orchestrator I’m trying to build works at a higher level than that. Instead of following the usual **main agent → multiple sub-agents** model, what I want is:

**Main coding agent → multiple worker coding agents.**

So, for example, a Claude Code brain/driver agent could delegate a task to a Codex worker agent.

You can see the difference here:

- On the left, the **brain coding agent** is the main interface with the user. It provides the feedback loop between the user and the brain agent.
- On the right, the **worker coding agent** runs a single-shot, task-based session. It receives a task from the brain and asynchronously returns the outcome and artifacts to the brain for review and approval.

In my mental modal:

- The user (me) works directly with the brain agent on solutions, task planning, and problem-solving. 
  - The user interacts through the TUI as a chat interface, similar to the chat-based interfaces we use today.
  - The brain agent provides the interface needed to spin up worker agents based on tasks agreed on by the user and the brain agent.
- A worker coding agent **should — and must — run a single-task session** .
  - That means there’s no back-and-forth interaction between the brain and the worker while the worker is executing its task receipt.
  - Once the worker session is done, it reports back to the brain agent for review.
  - There is an internal feedback loop between **brain ↔ worker** .

And of course, the user can interrupt and take control of that loop through HITL in the TUI:

- **user ↔ brain agent** .

This model enables a more autonomous SDLC while still keeping control. Every task should be reviewed by the brain, and HITL stays in charge of critical decisions.

The idea comes from **Apache Spark**, with its driver–worker model.

Hint: Spark’s **Adaptive Query Execution (AQE)** also gave me a lot of hints for building SpurCode’s planning mode. 

I’ll cover that in Part 2: the task planner for long-running tasks, planning, and execution.

Now that we have a basic understanding of what **SpurCode** is — a TUI coding agent orchestrator — we can move on to the more technical stuff.

## **ACP Protocol - Build on top of the giant shoulder**

**ACP (Agent Client Protocol)** is an open standard that connects AI coding agents to code editors and IDEs, such as Zed and JetBrains. To me, it looks like a USB protocol that allows an IDE or application to communicate with different coding agents.

### **Why ACP ?**

At the beginning, I was aware of my own resources and knowledge, and I kept asking myself:

- **Could I build an entire coding agent from scratch?** :
  - I felt it would be pretty foolish to believe I could build something better than existing, well-known coding agents like Claude Code and Codex. They have far more resources and are already widely adopted.
- **Could I fork an open-source coding agent like Codex CLI or OpenCode and build on top of it?** Maybe. But that would lead to another problem: keeping my fork in sync with the upstream version.

At the time, ACP was still at version 0.x.x. It wasn’t stable yet, but it looked very promising because many coding agents were starting to support it. And now, the list is much longer.

With ACP, I could immediately have a working coding agent running in the background. Since there were already multiple ACP-compatible agents, the main things I needed to handle were rendering and working with the ACP protocol. The protocol was defined, and [existing libraries](https://github.com/agentclientprotocol/) could help with it.

At the beginning, the architecture was very simple: a TUI to render and display messages from an ACP coding agent. SpurCode would also have a built-in MCP server to provide tools for the brain coding agent to delegate tasks to worker coding agents.

Initially, delegation was synchronous. It looked similar to the existing sub-agent model: the brain waited for the worker coding agent to finish

### **Rust - Rataui for TUI**

The TUI is the main interface between the user and the brain ACP coding agent.

Rust had been on my radar for a long time. When I started this project, I barely knew Rust. But I really wanted to learn it and experiment with the beauty of the language. That’s the main reason I chose Rust. Ratatui was also a well-known choice for building TUIs.

## Why do we need a middle layer after Spur-ACP?

Why we need midle layer after Spur-ACP for standardizing to create de-coupling layer Spur-Core to harmonize and standardize

The ACP could give out unified about protocol level , but the protocol itself have downside of version pragment and each vender have some change . So, Base on my experience , the down stream (the TUI) should isolate from the up stream protocol versin deviation , that why we need standardizing layer through the **spur-core** then after spur-core , you can have standard approach for render to display the ACP messages in TUI with Rataui

spur-core is more than just and de-couple layer in context of SpurCode system, but for part 1 , let tactical it as is.

At begining , my intending is cover many coding agent as much as possible , that why we have native , json stream or cli_wrapper , but eventually , now we most active develop on native acp

Because there are many of coding agent overther and each agents have slight different implement, the adjust will go through adapters/*.rs for standardize

#### 1. connection/native.rs — the native path: spec-shaped by construction

For a spec-compliant agent, the native connection does no shaping at all — that’s the point. It parses the official JSON-RPC frame directly into the official schema types and forwards:

```
   // native.rs (essence)
   session_notif_tx: broadcast::Sender<SessionNotification>,   
   // L1036 — connection-scoped
   // wire frame → serde → typed SessionNotification (agent_client_protocol schema)
   // → pass through only two *guards*, then broadcast:
   fn cache_session_notification_mode_update(...)   // L1355: side-cache CurrentModeUpdate
   fn should_forward_session_notification(...)      // L1372: drop notifs for closed sessions
```

Key idea: shaping = deserialization into spec types + filtering. The message is already the right shape the moment serde succeeds — nothing is rewritten, no vendor fields invented. The guards only side-cache state and suppress stale sessions; they never mutate the payload.

#### adapter/pi.rs — the adapter path: re-shaping a deviant vendor

Pi streams terminal output inside a non-standard _meta tunnel and never sets spec raw_output. The standardizer repairs it in place, in stream order:

```
 // pi.rs — the whole contract in ~30 lines
   pub fn standardize(&mut self, mut notification: SessionNotification) -> SessionNotification {
       if let SessionUpdate::ToolCallUpdate(ref mut update) = notification.update {
           self.standardize_tool_call_update(update);      // serial, per-stream = order-safe
       }
       notification                                        // same object, now spec-shaped
   }

   fn standardize_tool_call_update(&mut self, update: &mut ToolCallUpdate) {
       if let Some(data) = update.meta.get("terminal_output")
                                    .and_then(|v| v.get("data"))
                                    .and_then(Value::as_str) {
           let entry = self.terminal_output.entry(tool_call_id).or_default();
           append_capped(entry, data, ACCUMULATOR_CAP_BYTES);       // bounded accumulator
           update.fields.raw_output = Some(json!(entry.clone()));   // CUMULATIVE — TUI
                                                                    // replaces act text
           // _meta kept untouched: vendor diagnostic; consumers MUST NOT depend on it
       }
       if let Some(exit) = meta.get("terminal_exit") {              // vendor terminal signal →
           let (status, raw_output) = match exit_code {             //    spec ToolCallStatus
               Some(0) | None => (ToolCallStatus::Completed, accumulated),
               ...
```

- native.rs: “if the vendor speaks spec, pass the typed frame through untouched” — shaping costs zero. adapter/pi.rs: “if the vendor tunnels, keep a per-tool accumulator and rewrite the update so it looks

exactly like a spec agent would have” — so downstream, Pi and a spec-perfect agent are indistinguishable.

### spur-core : the core/heart of entire SpurCode. 

is transfer the language of acp to the SpurCode internal behavior system

#### The wrap (silver → gold, verbatim): notification_pump.rs:232

Every standardized ACP notification crosses into SPUR’s language like this — nested whole, not flattened:

```
   // notification_pump.rs (the pump drains the drain-loop’s callback)
   funnel.emit(SpurEventBody::AgentNotification {
       session: spur_session_id.clone(),          // SPUR’s own session identity
       notification: Box::new(notification),      // ← the ENTIRE spec ACP update,
   });                                            //   untouched, boxed inside
   snapshot.emitted = snapshot.emitted.saturating_add(1);
   fn emit_lag(funnel, session, skipped) { ... }  // and when the pipe lagged, SPUR
                                                  // says so *in its own language*
```

Note the shape: SPUR adds who (session) and keeps what (Box<SessionNotification>) — no field-mapping, no data loss, and the TUI/brain still gets the full spec event for rendering.

Now try to look into the architecture from other perspective. what is the difference between the brain coding agent and worker coding agent .

- Either brain or worker coding agent , if there are communicate through acp and the spur message/event traversal through spur-acp and spur-core before consume by whatever consumer app , then we trick it as the same , the same render engine or acp event through spur event envelop

## **Message/event ordering**

the ordering of message is supper critical here because of : what happen if there are wrong order : the TUI could not correct display the acp streaming message . what happen if there are end tune event come before the acp streaming message chunk. the render should stop the render/missing content

### spur-acp → spur-core boundary: subscribe-before-prompt + single-callback drain

*crates/spur-core/src/notification_drain.rs (drives every agent’s stream)*

```
// Subscribe BEFORE calling prompt() so we don't miss notifications
   // emitted during the async setup inside prompt().
   let mut notif_rx = connection.subscribe_session_notifications();
   ...
   loop {
       tokio::select! {
           biased;
           // stream frames one at a time → serial callback = per-agent FIFO
           Some(notif) = ... => on_notification(notif),      // ONE at a time
           None => ...                                        // stream closed →
               grace_deadline = Some(now + TRAILING_NOTIFICATION_GRACE),  // 100ms for stragglers
       }
   }
```

Guarantee: an agent’s own events are processed strictly one-by-one, in arrival order, and late frames still get numbered before the turn’s later events.

### spur-core → down stream 

```
   // Emitters (brain, workers, executors) all just enqueue:
   pub fn emit(&self, body: SpurEventBody) {
       let _ = self.tx.send(FunnelCommand::Event(body));   // unbounded mpsc
   }

   // ONE funnel task owns seq + publish:
   FunnelCommand::Event(body)

 => {
       let s = seq.fetch_add(1, Ordering::Relaxed);        // ① next ticket number
       let event = SpurEvent { occurred_at: SystemTime::now(), seq: s, body };
       lineage.lock().apply(&event);                       // ② in-memory projection
       let _ = broadcast_tx.send(event);                   // ③ publish — same task,
   }
```

All senders serialize onto a single mpsc queue; the single funnel task does number → project → broadcast as consecutive statements, so every consumer sees identical order. Cross-agent ordering

exists only here. (orchestrator.rs:392-399 documents it: “Monotonic sequence counter for the S2 funnel. The funnel task drains onto `event_tx`, stamping monotonic seq…”, bus created at :486 broadcast::channel(4096).)

The beauti of it is about : for TUI , you can easy to subscribe to the bus, the same logic to handle every acp message per coding agent . The same language SpurEvent from the Spur-Core

## Summary 

For Part 1: we target the high level architecture , the the fundamental about why ACP, the main backbone event driven/streaming processing architecture , standardizing and dedouplication between each layer where

- Spur-ACP best effort to give out shape-spec driven ACP message from different coding agent
- Spur-core the heart of the entire SpurCode try to translate the acp message , orchestrate, translate things into internal SpurEvent message/event bus for downstream
- Spur-TUI which is the main interface, subscribe to the 4096 bus SpurEvent for rendering and act as the main interface between user and the brain-agent , also observe the flow of the worker coding agent
