- Long AI tool-call chains tend to break down in the same few places — they run out of context, they can’t work in parallel, and once they’re running, nobody can see inside them or steer them. Computer science solved this kind of problem decades ago: delegate to subordinates that have their own context, run in parallel, and can be watched and controlled from outside.
- orbit, our in-process agent orchestration layer, comes down to a small, familiar set of tools — spawn, send, receive, kill, list, monitor — the same primitives a Unix shell has had since 1973.
- We didn’t invent this architecture. We borrowed it. Erlang’s actor model for supervision, POSIX for process lifecycle, pthread’s cooperative cancellation for safely killing a running agent, and the event-sourcing pattern behind Kafka and Temporal for durable replay.
- Cancellation is cooperative, never mid-token. Killing an AI agent while it’s mid-generation is the same problem as killing a thread mid-instruction — it corrupts whatever state it was touching. orbit only checks for a kill signal at safe boundaries: between LLM calls, between tool calls, after a chunk is yielded.
- The one genuinely new piece: the LLM is the supervisor. In Erlang, a supervisor is hand-written, deterministic code. In orbit, it’s whichever model is calling
spawn_agentandkill_agent— and it works because every spawn still has a hard mailbox limit, an abort signal, and a kill cascade underneath it.
If you’ve pushed an AI tool-call chain past a handful of steps, you already know where it breaks. The context window fills up before the task does. Ten independent sub-questions run one after another instead of all at once. And once the chain has been running for ten minutes, your only way to influence it is to wait for it to finish.
We ran into these problems while building out Vitru’s own agent layer. So we built orbit — an in-process system that lets one agent spawn others, talk to them, watch their progress, and cancel them. It’s small: a handful of tools, a mailbox, an event log, a worker loop. But the path from “what do we actually want” to “what’s actually shipping” cut through a surprising amount of computer-science history, and it’s worth writing down.
Why agent orchestration breaks down before it even starts
For about a year, the default pattern for AI agents has been the long tool-call chain: a system prompt, a set of tools, one continuous reasoning trace. Call a tool, read the result, call another, eventually answer. It works well for self-contained problems. It works badly the moment a problem has parts.
Here’s what actually breaks when the work doesn’t fit in one context window or one reasoning trace.
Context exhaustion. You can fit a lot into 200K tokens, but attention quality degrades well before you hit the limit. A task that has to ingest fifty documents and produce one synthesis hits a quality cliff somewhere in the middle — not at the edge you were watching for.
No parallelism. A tool-call chain is fundamentally serial. Ten independent sub-questions, done one after another, take ten times longer than they need to.
No observability or control. Once an agent has been running inside its own loop for ten minutes, the only way to influence it is to wait. You can’t redirect it mid-stream, and you can’t see what it’s working on without it volunteering that information.
It’s the same fix computer science has reached for, every time “this work is too big for one process” has come up over the last fifty years: delegate it to subordinates that have their own context, can run in parallel, and can be observed and controlled from the outside. That’s not a new idea. It’s just a new domain.
The napkin sketch: what a delegating agent actually needs
Before we wrote any code, we wrote a one-page sketch of what the agent-facing API needed to do. Looking at it now, it’s striking how directly each requirement maps onto a pattern that’s been solved before.
Quick tips for designing a subagent API
- Delegation is one tool call. The parent spawns a subordinate with its own model, its own instructions, its own conversation —
spawn_agent(config, prompt)— and gets back a handle. No black-box framework, no static graph an engineer pre-wired. - Most of a child’s work stays private. Scratch reasoning and intermediate tool calls live in the child’s own history. Only what it explicitly hands back — via
send_message, flagged as final or not — is visible to the parent. - Status should be bounded, not a full transcript. A
get_run_statuscall returns a snapshot: the last bit of output, the last few tool calls, the lifecycle state. Bounded, so a frequent check doesn’t drag the whole conversation through the wire every time. - Push, not just pull.
set_heartbeat(seconds)schedules periodic updates so the parent doesn’t have to poll — throttled, and only sent when something has actually changed. - Killing needs two speeds. Graceful lets the in-flight call finish so you don’t waste the tokens you already paid for. Force aborts immediately. Children cascade-die by default, but any child can be marked to survive and get promoted to a free-standing agent.
- The whole thing has to be a thin layer. No modification to the host application. If it’s built right, ripping it out is one delete.
Looked at together, this is already close to a complete supervised-process model. Spawns are processes. The mailbox is a Unix pipe. kill_agent is kill(2). We didn’t set out to reinvent Unix — the shape just followed from the problem.
Where we borrowed the architecture
Once the sketch hardened into something we had to actually ship, we spent time looking at how similar problems had already been solved, and used that research to shape our own approach. Here’s what each piece grew into, credit where it’s due.
Erlang/OTP — actors and supervision
A spawn can only be reached through send_message. Its parent is linked: killing the parent kills its children by default. A child that should survive is explicitly marked on_kill: 'detach' — the same opt-out Erlang uses when a process traps its parent’s exit signal. Once you commit to “the only legitimate channel between two spawns is send_message,” you stop being tempted to share state or a common transcript — the group-chat pattern some multi-agent frameworks use, and the shared-mutable-state problem computer science has been solving since the 1970s.
POSIX — process lifecycle
orbit’s states are POSIX-shaped: queued, running, awaiting_input, done, errored, killed, detached. Termination is SIGTERM-then-SIGKILL — graceful kill first, with a grace period (default 30 seconds, capped at five minutes) before orbit escalates to force. Spawn IDs are never reused, because PID reuse is a decades-old footgun: a stale reference suddenly points at an unrelated new process.
pthread — cooperative cancellation
Asynchronous thread cancellation in C is a known disaster — killing a thread mid-instruction leaves shared state in an arbitrary condition. POSIX’s answer was deferred cancellation: the thread only checks for a kill request at defined checkpoints. LLM streaming has the identical problem — abort mid-token and you bill for half a token and leave the state machine confused. orbit’s cancellation checks only at safe boundaries: between LLM calls, between tool calls, after a chunk is yielded.
Kafka and Temporal — event sourcing
A spawn generates a lot of activity: messages, tool calls, status transitions. orbit logs all of it as an append-only event stream, then keeps a small, denormalized snapshot for fast reads. A status check reads the snapshot. “Show me what this spawn actually did” walks the log. Replay after a crash re-prompts with the inputs that produced the interrupted turn rather than trying to deterministically replay a non-deterministic LLM call — the same approach Temporal uses for non-deterministic activities.
CSP and Go channels — bounded mailboxes
Default capacity: 100 messages, overflow mode block, with a five-minute watchdog. This isn’t a memory concern. Unbounded queues are how production systems quietly degrade for hours before anyone notices. A bounded mailbox turns that into an immediate, visible stall instead.
Temporal — throttled heartbeats
A pulse at most once per interval, and only when something has actually changed. The naive version — report every loop iteration — turns status updates into the single biggest cost line on a long-running task.
Six tools, the same six primitives Unix has had since 1973
Stare at orbit’s surface area long enough and you see what’s already there:
spawn_agent→forksend_message→writeto a pipe- mailbox receive between turns →
wait kill_agent→kill(2)list_spawns→psget_run_status/set_heartbeat→ monitoring
The bet underneath this: delegation should be a tool an LLM reasons its way into using, not a black-box framework feature baked into a graph an engineer drew ahead of time. Give the model the right verbs, and the orchestration emerges from its own reasoning. Anthropic’s Task tool and OpenAI’s Agents SDK land on the same six primitives independently. That convergence isn’t an accident — it’s the same problem, solved the same way, twice.
The one genuinely new idea: the LLM is the supervisor
If there’s one piece of orbit that isn’t a direct trace of an older pattern, it’s putting the LLM itself in the supervisor role. In Erlang, a supervisor is hand-written, deterministic code. In orbit, the supervisor is whichever model is calling spawn_agent and kill_agent — its supervision strategy is whatever its system prompt and reasoning produce at runtime.
On a strict reading of supervision-tree theory, this shouldn’t work. Supervisors are supposed to be reliable and simple. LLMs are neither. It works anyway, because the failure modes are different. An LLM supervisor isn’t going to crash or hang — it’s going to make a decision that’s locally reasonable and globally suboptimal. And the cost of that is bounded, because every spawn still carries a mailbox limit, an abort signal, a watchdog timeout, and a kill cascade underneath it, regardless of what the supervisor decides.
orbit is the kernel. The LLM is init. The kernel provides fork, exec, signal, and pipe, with predictable semantics and a bounded blast radius. What init does with those primitives is init‘s problem — and if init makes a bad call, only its descendants suffer. The kernel’s invariants hold either way.
What this unlocks
Once this layer exists, a few things that are usually research demos start to look like plain infrastructure features:
- Persistent assistants that span sessions. A spawned agent doesn’t have to die when a chat closes. Mark it
detachand it sits inawaiting_input, ready to pick up where it left off hours or days later. - Teams assembled at runtime. A parent agent decides a problem needs a researcher, a writer, and a critic. It spawns them with role-specific prompts, fans them out in parallel, and kills them the moment the work is done. The team exists for exactly as long as the task does.
- Subagents with their own tool access. Each spawn gets its own toolset. A child can reach a system its parent can’t — a Unix-style capability set, scoped per role instead of granted globally.
- Long-running background work. A spawn can keep running while you do something else, filling its mailbox with updates you read when you’re ready, without blocking your own conversation.
- Auditability that survives the model. Every event lives in an append-only log — every message, every tool call, every status change, timestamped. Six months from now, you don’t ask the model why it did something. You read the log.
Curious what this looks like running against your own models and workflows? Book a demo and we’ll walk through how Vitru’s agents use this orchestration layer in production — or see the agents live.
“The interesting thing about building this layer wasn’t inventing anything. It was recognizing, over and over, that we’d already seen the problem somewhere — in Erlang, in POSIX, in Kafka — and that the lesson from that older system still applied here, mostly unchanged.”
— Ian Arden, Founder, ADAIA
FAQ
What is an AI subagent system?
A layer that lets one AI agent spawn other agents, each with its own context and conversation, communicate with them through a bounded mailbox, monitor their progress, and terminate them — instead of running everything through one long, serial tool-call chain.
Why can’t you just make one longer tool-call chain instead of spawning subagents?
A single chain is serial and shares one context window. Independent sub-tasks run one after another instead of in parallel, and a long task eventually hits a quality cliff well before it hits the token limit. Delegating to subagents with separate contexts fixes both.
What actually happens when you kill an AI agent mid-task?
If the kill is forced, the stream aborts immediately. If it’s graceful — the default — the in-flight model call is allowed to finish so the tokens already spent aren’t wasted, and the mailbox closes after a grace period (30 seconds by default, capped at five minutes).
Why build this in-process instead of as a separate service?
Because the boundary is the contract, not the network. Communicating through a defined protocol keeps the interface testable, mockable, and swappable — so if this layer ever needs to run as its own service, the code that calls it doesn’t have to change. Only the transport does.
How is this different from a shared multi-agent group chat?
In a group-chat pattern, multiple agents write into one shared transcript. orbit rejects that by construction — the only channel between two spawns is a direct message to a bounded mailbox. It scales better and avoids the shared-mutable-state bugs that come with a common transcript.
Can a spawned agent survive after its parent is killed?
Yes, if it’s explicitly marked to detach. By default, killing a parent cascades to its children — the same link-and-trap behavior Erlang uses — but a child marked on_kill: 'detach' gets promoted to a free-standing agent instead of dying with its parent.
The takeaway
The interesting part of building orbit wasn’t inventing anything new. It was recognizing, section by section, that we’d already seen the problem somewhere — in Erlang, in POSIX, in Kafka — and that the answer from that older domain still held up, mostly unchanged. That’s most of what engineering is, once you stop trying to be clever about it.
Six tools, an event log, a bounded mailbox, a cooperative cancel, and a long bibliography. The bibliography is the point.
Want to see how this orchestration layer shows up in the agents Vitru runs on real projects? Book a demo and we’ll walk through it — or explore the agents directly.