Skip to content

The agent answers in batches, in request order

A tick activates thousands of personas at once, and inference is batch-first: complete(requests) -> outcomes, one outcome per request in request order, failures as outcomes rather than exceptions (ADR 0023). elicitation keeps the same contract. The agent does too: turns(jobs) -> outcomes, where a job is one persona with its state and its presentation, and an outcome is either a completed turn or a recorded turn failure.

A single turn is a batch of one. Nothing is merged into a shared prompt — every persona still gets its own, conditioned on itself alone.

The alternative — one call per persona — forces the runner to assemble batches itself if the tier coalescing and rate limiting built into inference are to be used at all. That puts prompt assembly outside the module that owns the conditioning invariant, which ADR-worthy or not is the one thing this codebase refuses to let happen.

Considered options

turn(persona, impression, view) -> Reaction per persona, as sketched in the architecture, was rejected: it either wastes the batching inference exists to provide, or leaks batch assembly into the runner. Splitting the agent into plan_turn and parse_turn so the runner batches the dispatch between them was rejected because the guardrail check needs both halves — it compares parsed output against the context that produced it — and splitting them puts one invariant on two sides of a module boundary.

Consequences

All tier-A turns in a tick coalesce into real batches, and the adaptive rate limiter sees them as such. One persona's failure cannot end a tick: it is an outcome beside the others, and the policy for it is the contract rather than something the runner invents. The runner stays thin — it hands a tick's jobs over and receives outcomes — and never builds a prompt. The cost is that the simple mental model is gone from the signature; tests and the docs keep turns([job])[0] readable for the single-persona case.