Skip to content

Inference Harness

The Harness is the Orchestrator — the inference cycle at the centre of every run. It assembles the context, calls the model, runs whatever tools the model asks for, feeds the results back, and repeats until the model is done. It is deliberately ignorant of email: it never knows whether the work came from an inbound message or a scheduled duty, only that it has an agent, some context, and a set of tools to offer.

The tool loop

A run is a bounded loop. Each turn the model either answers or asks to use tools; if it asks, the Harness runs them and hands the results back for the next turn.

assemble context (system prompt · retrieved data · history)


 ┌─► call the model
 │      │
 │      ├─ no tool calls ─► done, return the reply
 │      │
 │      └─ tool calls ─► run each tool ─► feed results back ──┐
 │                                                            │
 └────────────────────────────────────────────────────────  │
        (repeat, up to max_tool_turns rounds)  ◄─────────────┘

Two things bound the loop so it can't run away:

  • A turn budget. max_tool_turns (default 8) caps how many tool-calling rounds a single run may take. On the last allowed round the Harness stops and returns whatever the model last produced.
  • A scoped tool set. The model is only ever offered the agent's effective tools — its granted set minus anything an operator has switched off — so a run can only reach for what the agent is allowed to do. (See Personas for how scope is declared.)

Every step emits a lifecycle event — run started, tool called, tool completed or failed, run completed — which is how hooks, the live introspection panel, and tracing observe a run without the loop knowing they exist.

Two front doors, one core

The inference loop is shared; only what drives a run differs. The Harness is an abstract core with two concrete entry points, chosen at the moment work arrives:

  • Reactive — answering an email. When a customer writes to Sundae, the run is seeded with the inbound message body, replays the thread's history, and (for console conversations) streams tokens back live.
  • Proactive — running a scheduled duty. When Scout's morning report comes due, there is no inbound message. The whole opening turn is assembled from the duty's own instructions instead. See the Scheduler.

Because the branch between them is decided up front — the caller picks which front door — the loop itself has no "is this an email or a schedule?" conditionals. Both paths persist what the agent did, so a later turn can remember earlier tool use, and an operator can audit a scheduled run after the fact.

Context assembly

The Harness doesn't build context itself; it delegates that to the agent's resolver chain. Each resolver contributes a piece of the picture — the system prompt, the customer's record, the menu, the conversation so far — and the chain concatenates them, in order, into the message list the model sees. The reactive and proactive paths use different chains, so an agent can present itself differently when answering mail than when acting on its own initiative.

Every tool and resolver in a run shares one immutable context object carrying the agent, a frozen configuration snapshot, and the correlation id that ties the run's events together. It also carries the affordance for sending mail — which is how a tool reaches the Gateway without ever touching a transport.

Provider-agnostic by design

The Harness only ever speaks a normalized request and response. The actual model call is the Inference layer's job, behind a provider adapter. Swapping Sundae from one model to another is a configuration change; the loop, the tools, and the resolvers are untouched.

  • Resolvers — how the context for each run is assembled.
  • Tools — the actions the loop dispatches.
  • Scheduler — the proactive front door.
  • Tracing — snapshotting each turn for review.