Engineering
Long Horizon Agents
How Unitbase built a system that allows long-running and large-context agents to operate specialized workflows.
Unitbase Team 11 min read
When we first put agents to work on bank reconciliation, we built a hierarchical multi-agent system. A coordinator sat at the top, with specialized subagents beneath it for the bank feed, the general ledger, matching and journal entries. The coordinator broke each request into subtasks, routed them to the right subagent and combined what came back.
That design fit the work at the time. Requests were narrow, like matching a deposit or pulling a balance, and the models of the time handled a narrow request reliably in one or two tool calls. As frontier models matured, customers started handing the agent the hard part of reconciliation: working through exceptions, tracing batched deposits and explaining why a period doesn't tie. That work means planning, branching and executing across many steps, and the properties that had made the system fast and predictable became its limits: rigid routing, shallow context and little room for the model to reason about how to approach a problem.
Why we needed a new architecture
As the agent took on longer work, several constraints of the coordinator design surfaced, and each one got worse as tasks grew more complex.
The coordinator reasoned over summaries
Each subagent ran in its own LLM context. The coordinator routed work to them and received summaries back, but it never saw raw tool outputs, intermediate reasoning, or error details. It made every downstream decision from a lossy, secondhand view of its own work, which led to missed connections between the bank side and the ledger side, repeated lookups for data another subagent had already pulled, and no way to recover cleanly when something failed in the middle of a task.
Limited iteration depth
Even when routing worked, the system was tuned for short tasks of a few steps: look something up, read it, answer. Its iteration budget was low and its timeout was conservative. Work that needed sustained reasoning across several sources, such as "reconcile the operating accounts for all three entities" or "explain why September does not tie", would regularly hit the ceiling before it produced a complete answer.
Models outgrew the box
The multi-agent design was not an accident; it was a workaround. Earlier generations of LLMs struggled with large shared contexts and long tool catalogs, and dividing the problem among specialized subagents kept each context manageable and tool selection accurate. Frontier models changed that on two fronts: they handle longer contexts and larger tool surfaces reliably, and reasoning models can now plan, reflect, and iterate over many steps inside a single coherent context. The old architecture could take advantage of neither, because it fragmented the very context those models needed to think in.
The long-horizon architecture
The new engine addresses these problems with a different design: one LLM, one context, one iterative loop.
The name comes from the idea of an agent's horizon, meaning how far into a task it can plan, reason, and act before it loses the thread. The coordinator design had a short horizon, built for quick answers to single questions. The new engine is designed for a long one. It gives the model the room, in both time and context, to plan a multi-step approach, adapt when an intermediate step fails, and carry a workflow through a long run of iterations without losing track of what it has already established.
The core loop
At the center of the engine is a simple loop:
- Build the system prompt. Assemble the context for the request: the company, its entities and accounts, the period, the conversation so far, and any skill that applies.
- Select and prepare tools. Load the most-used tools at the top level and expose every connected system through its namespaced tools, replacing what used to be subagents.
- Loop, up to the iteration limit:
- Call the LLM with the full conversation state and the available tools.
- If it returns tool calls, execute them (in parallel where they are independent), add the results to the context, and continue.
- If it returns a final answer, stop.
- Respond. Deliver the result with every figure linked to the source record it came from.
Adaptive reasoning
The engine uses adaptive reasoning effort to match the depth of thinking to the difficulty of the request. For a straightforward lookup, such as "what cleared on the 14th?", the model applies minimal reasoning and responds quickly. For a multi-step investigation, such as "compare the unreconciled balance across the last three closes and tell me what keeps recurring", it engages deeper reasoning to plan its approach, evaluate intermediate results, and synthesize a thorough answer.
This avoids the trade-off a single fixed configuration forces, where an agent is either fast and shallow or thorough and slow. The same architecture handles both ends by letting the model decide, turn by turn, how much reasoning the request is worth.
Flattened tool architecture
The single biggest change in the new engine is how tools are exposed to the model. The coordinator design wrapped each system in its own subagent: a bank agent, a ledger agent, a matching agent, and so on. The coordinator's job was to decompose a request and hand subtasks to the right subagent, and each subagent then chose among its own actions. Because every hand-off went to a separate LLM with its own prompt and context, information was lost at each hop: the subagent saw only a summarized version of the coordinator's task, the coordinator saw only a summarized version of the subagent's findings, and intermediate reasoning, tool errors, and raw tool outputs were compressed or dropped along the way. The coordinator often had to ask again, guess at what had failed, or work from a synopsis of what the subagent had actually seen, which made it harder to recover from errors and harder to chain results from one system into the next. The design was also expensive to maintain, since every model upgrade became a separate migration for each subagent, each needing its own round of re-tuning and re-evaluation.
The new engine replaces this with two ideas that work together. The first is flattening: collapsing every system's capabilities into a single, uniformly named tool surface that the model calls itself, so that nothing is paraphrased through a second agent. The second is progressive disclosure: exposing that surface to the model on demand, so that we do not pay up front for every tool's schema as we connect more systems or add finer-grained tools within one.
How it works
- Flattening: one unified tool surface. Every operation across the systems we connect to is exposed as a typed, namespaced action, such as
bank__list_transactionsorledger__get_gl_detail, that the engine's LLM invokes directly, with no intermediary agent. The model now sees each tool's raw arguments, raw response, and raw error rather than a subagent's paraphrase. When a call fails or returns something unexpected, the same LLM that decided to make the call reads the failure and decides what to do about it: retry with different arguments, fall back to a related tool, or tell the user what is missing. Recovery becomes part of the reasoning loop, where before the information needed for it was summarized between stages and often lost. - Progressive disclosure: pay for what you use. Sending the schemas for hundreds of flattened tools on every iteration would add cost to every call, and it would degrade the model's tool-selection accuracy as the catalog grows. So each namespace is collapsed to two meta-tools:
{namespace}__get_tool_schemareturns the full input schema for one specific tool on demand. Its description carries a one-line summary of every tool in the namespace, so the model can scan what is available without paying for the full schemas.{namespace}__invoke_toolexecutes a tool in that namespace by name, with its arguments.
Alongside each namespace we ship a SKILL.md, a short, hand-written guide to the system-specific logic the model benefits from: which tool to reach for in which situation, how the system's concepts map to what the user is asking for, the common multi-step recipes, and the pitfalls. The skill file holds the per-system expertise that used to live implicitly inside a subagent's prompt, while the meta-tools provide a uniform way to invoke anything.
The most heavily used tools (search, the agent's task list, file read and write, record linking, memory retrieval) stay at the top level with their schemas always loaded, because they are called on nearly every turn and are worth keeping resident in the prompt.
Progressive disclosure does introduce a discovery step: for a tool the model has not used recently, it first calls get_tool_schema and then invoke_tool. We accept that cost deliberately. It is real but bounded, since a schema is fetched once per tool per task, and the SKILL.md usually lets the model go straight to invoke_tool without a separate fetch.
Why it helps
- End-to-end visibility, end-to-end recovery. The same LLM that picks a tool also reads its raw response and its raw error, in the same context in which it planned the call. Selection happens with the full task in view, not a subagent's restatement of it, so the model picks the right tool more often on the first attempt, and when something fails or comes back unexpected it can retry, fall back, or change course without first reconstructing what a subagent meant.
- No per-system schema tax. The old architecture paid for a system's full schema bundle every time its subagent was invoked, and that bundle grew whenever the system gained a tool. Flattening removes the repeated payment, and progressive disclosure means that even within one system we pay only for the tools the model actually reaches for in a given task.
- One model migration, not many. A new frontier model is evaluated and rolled out once, at the engine, and every system's tools move with it. What used to be a separate re-tuning and re-evaluation pass per subagent is now a single workstream.
Context window management
A reasoning loop that runs for this many iterations puts real pressure on the context window: older tool outputs accumulate, the token budget tightens, and the model has more to sift through on every call. The engine manages this along two dimensions, compaction within a single context and decomposition across several contexts when a single one is not the right unit of work.
Context compaction
A dedicated compaction service checks the context before each model call. When the conversation approaches the token limit, it compacts in a single pass: older tool outputs are trimmed or summarized while recent results are kept at full resolution. Pruned outputs are not discarded; they are offloaded so the model can read them back on demand if it later needs the detail. This keeps long, multi-step runs inside the context window without losing the reasoning the model has already done.
Task decomposition through child instances
Some tasks are not deep so much as wide. A request like "find out why the portfolio is out of balance this month, across the five operating accounts" breaks into five independent strands of investigation, and loading the full evidence for all five into one context would exhaust the window long before the model could synthesize an answer.
For work like this, the engine spawns a child instance of itself for each strand. Every child runs the same one-LLM, one-context loop on its assigned subtask, with its own clean context and the most relevant skill where one exists. Strands run concurrently within a single turn, so the slowest strand sets the response time rather than the sum of them. The parent receives a finished, self-contained result from each child and synthesizes them, without ever having to hold every intermediate finding from every strand in its own context.
This is a different role from the one subagents played in the previous architecture. The old subagents were specialists in the routing path: every tool call passed through one, and the coordinator never saw what they saw. Child instances are full reasoning loops, spawned on demand to own a complete piece of work and return it finished. The parallelism is a side effect; the primary motivation is keeping each context focused on what it needs to think about.
Prompt assembly and caching
A long-horizon run sends the model the same system prompt, the same skill instructions, and a growing conversation history on every iteration. Over a long run that adds up to a very large number of tokens that the provider would otherwise process again on each call. To avoid paying for them more than once, the engine assembles every prompt from layers ordered from most stable to most volatile:
- Static system prompt, identical across every run
- Stable session context: the company, the user, the period, and skill instructions, fixed for the duration of a session
- Conversation history, which grows over time and changes only when compaction runs
- Turn-dependent context: the current iteration's tool results and reasoning state
Because each layer changes less often than the one after it, the longest possible prefix stays byte-identical from one model call to the next. Providers with automatic prefix caching reuse it without any further work; for providers where caching is opt-in, the assembler places explicit cache markers at the system, stable-context, and last-history boundaries. The result is that on most iterations only the newest tokens, typically a tool result and the model's next reasoning step, have to be processed from scratch. Because compaction runs in occasional passes rather than on every call, the cache is rebuilt only at those points and stays warm in between.
This is not a context-management technique, since the model sees the same context with or without caching. It is a cost and latency saving that grows with the number of iterations: the longer a task runs, the more the cache pays off.
The skills system
Separate from the per-system SKILL.md guides, the engine includes a framework of workflow skills: pre-authored, domain-specific prompt templates for tasks that recur in the work. When a request matches a skill (period-end reconciliation, for example, or investigating an out-of-balance account), that skill injects a proven approach into the system prompt.
Each skill provides:
- A step-by-step workflow that encodes how the task is done well
- Domain knowledge about which tools to call and in what order
- A tested structure for the final output
Skills are individually feature-flagged, configurable per customer, and loaded at runtime. They hold what we have learned about what works for a specific kind of task, condensed into reusable templates that guide the reasoning loop toward a better result.
Observability for long-running agent tasks
A reasoning loop that spans dozens of iterations and tool calls produces trace data that request-scoped logging was never designed to hold. We instrument the engine with structured LLM tracing, capturing every decision, tool invocation, latency breakdown, and token cost as a hierarchical trace tree. An engineer can drill from the top-level span down through each reasoning iteration to find a root cause: a wrong tool selection, a silent failure, retries that burned budget, or context pressure building across iterations. That trajectory-level view is what makes it possible to debug a 40-step task the way you would debug a call across distributed services, with reasoning steps in place of the services and tool calls in place of the RPCs.