The one runtime: the durable segment loop on Inngest, the AgentExecutor checks, and the Agno agent loop that reasons, calls tools and answers.
1 min read Updated Oct 16, 2026
05 · Runtime
The runtime executes every run that Runs admits. This page explains the core agent loop: how one agent run becomes durable segments, what one segment checks, and how the Agno agent reasons, calls tools and answers. The workflow executor shares the same durable shell, and its own page will cover it.
Agno is the only agent brain. Inngest gives durability, and AgencyCore code gives policy, budgets and storage. Agno owns one job: the model turn and the tool-call loop inside one segment.
One Inngest function holds a run. It runs the run as segments, and each segment is one durable step. Inside a segment, AgentExecutor checks the run, and the Agno agent loops on model turns and tool calls until it answers, pauses or reaches a ceiling. Numbered badges mark the issues at the end of this page.
One Inngest function, agent/run.execute in runtime/inngest/execute.py, owns one run from claim to finalize.
Admission check and claim. The function checks admission, then moves the run from queued to running. The claim reads max_segments from the frozen snapshot one time, so the loop bound cannot move during a replay.
Segment loop. Each segment is the step segment.N. An asyncio timer stops the segment at 90 seconds (MAX_SEGMENT_DURATION_S), inside the 120 second step budget. The last 30 seconds let the segment save its session and close its spans.
Outcome. A segment answers an ExecutionOutcome:
succeeded, failed or cancelled ends the loop.
running means the timer cut the segment. The function runs advance.N and starts the next segment, which continues from the saved conversation.
waiting means a tool call needs a person. The function runs wait.mark.N, then advance.N, then waits for agent/approval.resolved. The wait times out at the approval expiry.
Finalize. One step applies the terminal status and sends the run end event. A terminal status has one writer.
Each status write is its own step. Three writes in one step are not atomic, and a retry of a half-written step leaves the run at waiting with no wait and no writer.
AgentExecutor in runtime/execution/agent.py runs before every model call of a segment:
Re-read the run. Stop if the status is not running, so a person who pressed stop does not pay for another turn.
Parse the frozen snapshot and the frozen principal. A defect here is terminal, because every retry reads the same snapshot.
Check accrual: the cost of the whole run tree, and the organization day cap.
Compute the remainders. Turns and tool calls already spent in this run come from the span tree, and wall clock comes from started_at. Each segment gets what is left, never the full ceiling.
Build a context brief on segment 0, and again after a wait when the policy says refresh_on_resume.
Filter the frozen tools by the frozen grant.
Build the AgentExecutionRequest and call AgentRuntime.execute().
After the segment, the executor maps the stop reason to an outcome. It also cancels approvals that no outcome waits on. A model can propose several write calls in one turn, but only one approval can park the run.
AgnoAgentRuntime.execute() builds a new Agno Agent for each segment. Agno uses an InMemoryDb, and the runtime fills it from agent.sessions and writes it back at the end. Agno telemetry is off, because the default sends the run id to os-api.agno.com.
Step
What happens
1 · Model turn
arun() starts the turn, or acontinue_run() continues a paused run. The output is streamed. AgnoEventBridge opens one llm span and writes one usage row per turn, and it publishes text.delta and tool.updated.
2a · Read tool
The tool runs inside the Agno loop. AgnoToolAdapter calls ToolInvoker, and the result goes back to the model in the same turn.
2b · Write or send tool
The tool is declared for external execution, so Agno pauses. The runtime decides the pause in process through ToolInvoker, then continues the run. Only a call that policy sends to a person ends the segment as needs_approval. A denial goes back to the model as the call result.
3 · Answer checks
The final answer must pass the output schema, the citation check (every cited id must come from a tool result) and the receipt binding. A failed answer gets up to two correction turns with tools, then one fix turn with no tools.
The ceilings apply before every call, not one time per turn. One turn can hold ten write proposals, and a check per turn would let all ten run on a remainder of one. A bound run that reaches a ceiling while it holds receipts gets one closing turn with no tools, so it can answer from the work it already did.
AgentExecutionResult.stop has five values, and AgentExecutor branches on nothing else.
Stop
Run outcome
completed
succeeded with the result
ceiling
succeeded with partial_reason set to limit_reached or budget_exhausted
needs_approval
waiting on the approval, with its expiry
cancelled
cancelled, and nothing more is written
failed
failed with a named RunError code
A retryable fault, such as a vendor 5xx or a database outage, raises instead of failing. Inngest replays the whole step, and the idempotency journal stops a tool effect from happening two times.
runtime/agent/agno/runtime.py, with adapter.py, bridge.py, segment.py and session.py
Segment checks and outcome mapping
runtime/execution/agent.py
Run kind dispatch
runtime/execution/executor.py
The durable shell
runtime/inngest/execute.py
Ceilings and step budgets
shared/ceilings.py
Model pairs that may publish
runtime/definitions/model_registry.py
The tool call pipeline
services/tools/default_invoker.py
The Front Door is a second Agno call site, in entry_control/front_door/agno.py. It makes single-turn calls with no tools, so it does not use the segment loop.
max_run_duration counts from started_at, and waits count too (_elapsed in runtime/execution/agent.py). _expiry in governance/policy/approvals/service.py sets an approval expiry to min(now + 24 h, run deadline). An agent with max_run_duration_s: 300, such as the CRM question agent or the company finder agent, gives a person less than 5 minutes to answer. After the approval, the next segment gets only the time that is left.
Fix: stop the wall clock while a run waits, or give each wait its own deadline that the run deadline does not cap.
A read tool runs inside the Agno loop, and that loop has no pause. When policy answers require_approval for a read tool, the run fails with read_tool_needs_approval (runtime/agent/agno/runtime.py:1607).
Fix: refuse a policy rule that gates a read tool at publish time, or declare a gated read tool for external execution so that it pauses as a write does.
The runtime sets num_history_runs = max_segments (runtime/agent/agno/runtime.py:376), so the window counts Agno runs. One segment can make up to four Agno runs: the first turn, two correction turns, and a fix or closing turn. Agno includes only messages from the last num_history_runs runs. A long run can then lose its first task and its early tool results. The citation check (_read_ids) also reads that history, so it can refuse an id that a dropped turn read. No test or live run has shown this yet.
Fix: set the window from the number of Agno runs, not segments, or remove the bound. Add a test for a run with corrections across several segments.
runtime/agent/agno/runtime.py:915 says advance_segment has no caller and that every later segment write is refused. runtime/inngest/execute.py:1726 calls it.
runtime/agent/agno/runtime.py:428 says no tool executes before ENG-2080. The invoker runs tools today.
The _wait_out docstring in runtime/inngest/execute.py:1922 says that no runtime answers needs_approval and that the recovery sweep is not built. Both exist now.
These comments describe a system that no longer exists, so they mislead the next reader.