Skip to main content
Advanced/compatible programmatic examples. Fragments illustrate mechanisms; HostEnv or workspace does not provide isolation. New projects start with Quickstart, Session and explicit resource configuration.
Engine is the single execution kernel for all QitOS agent workflows. It runs the step loop, coordinates your AgentModule hooks, dispatches tool calls to the ToolRegistry for execution, applies critics (post-step evaluators that can approve, stop, or retry the step), checks stop conditions, and writes trace artifacts (persistent records like steps.jsonl and events.jsonl that capture what happened during the run). You interact with it directly only when you need control beyond what agent.run() provides.
QitOS enforces a single-kernel rule: there is exactly one Engine per run. Extensions like parsers, critics, memory adapters, and toolkits attach to this pipeline — they do not introduce a second execution loop.

How the loop works

Each step follows a fixed sequence:
  1. prepare — agent.prepare(state) formats state into the model-ready prompt text.
  2. decide — agent.decide(state, observation) is checked first; if it returns None, the Engine calls the LLM via the configured parser.
  3. act — Tool calls in the decision’s actions list are executed against the ToolRegistry.
  4. reduce — agent.reduce(state, observation, decision) updates state with the new observation.
  5. critics — Any registered Critic instances (post-step evaluators that can approve, stop, or retry the step) evaluate the step; they can trigger a stop or retry.
  6. check_stop — Budget exhaustion, FinalResultCriteria, agent.should_stop(), and any custom StopCriteria are evaluated.
  7. trace — The step record and events are written to the TraceWriter.

Constructor

Illustrative fragment (not a standalone program; use the complete example linked on this page).
Prefer calling agent.run() for single-run workflows. Use Engine directly when you need to reuse an Engine across multiple runs or configure hooks dynamically between runs.

Docker environment paths and variables

DockerEnv(container_env={...}) passes an explicit environment mapping when it creates a container. Relative filesystem paths resolve under the configured container workdir; absolute paths are interpreted as absolute paths inside the container. QitOS does not copy benchmark- or task-specific host environment variables implicitly.

Protocol resolution

Engine resolves the model protocol once per run using the following precedence:
  1. The protocol passed directly to Engine.
  2. agent.model_protocol.
  3. A protocol inferred from the Engine or Agent parser.
  4. agent.llm.qitos_protocol, then agent.llm.qitos_harness_metadata["protocol"].
  5. A protocol inferred from the model name.
  6. The framework default, react_text_v1.
The model-level declarations in step 4 are attached by build_model_for_preset(...). This means direct construction such as Engine(agent=agent) preserves an explicitly selected family preset even when the provider model name is an alias that QitOS cannot infer. Unknown model-level protocol identifiers are ignored so the existing model-name and framework fallbacks remain available. Traces record model_qitos_protocol or model_harness_metadata as the resolution source when either declaration is selected.

Empty model responses

After a model call, the Engine records the normalized response and gives AgentModule.interpret_model_response() the first opportunity to turn it into a decision. If the hook returns None and the response contains neither non-whitespace text nor tool calls, the Engine classifies the result as a model_error before it reaches the parser. The default recovery path retries once; a second consecutive empty response stops the run with unrecoverable_error instead of consuming the remaining step budget as parser wait decisions. The model_output event and StepRecord.model_response still retain the response’s finish_reason, usage, model, and provider fields. This guard does not change native tool-call responses, explicit Decision.wait values, or parser feedback for non-empty malformed model output.

Engine.run(task)

Illustrative fragment (not a standalone program; use the complete example linked on this page).
Accepts a plain string objective or a structured Task object. Returns an EngineResult. When you pass a Task, the Engine extracts the budget from task.budget and overrides the Engine’s own budget for that run. It also orchestrates resource staging and environment lifecycle (reset, observe, close) automatically.

EngineResult

Illustrative fragment (not a standalone program; use the complete example linked on this page).
Typical usage: Illustrative fragment (not a standalone program; use the complete example linked on this page).

Hooks

Hooks observe and react to lifecycle events without modifying Engine internals. They implement EngineHook and are called at on_before_step and on_after_step boundaries. Illustrative fragment (not a standalone program; use the complete example linked on this page).
You can also pass hooks at construction time via the hooks parameter, or at run time via agent.run(hooks=[...]).

Runtime exception reporting

Handled exceptions — anything the recovery layer catches during DECIDE/ACT, plus fatal init_state and env-setup failures — are reported through Engine._report_runtime_exception(): one stderr line with the full traceback, a RECOVER trace event carrying the structured payload, and (when QITOS_ERROR_LOG or QITOS_TRACE_DIR is set) an appended step_error.log. The last payload is exposed as last_error in the EngineResult run summary, so a run that ends with unrecoverable_error keeps usable diagnostics even under redirected output. Tool-call loop detection can be disabled for long-running agents that intentionally repeat calls: ContextConfig(tool_call_loop_detection_enabled=False) (default True, with loop_max_repeats controlling the threshold).

Budget exhaustion

When the step budget, wall-clock time limit, or token budget is exceeded, the Engine sets state.stop_reason to the appropriate value and emits an END event. The run terminates gracefully and EngineResult is still returned — inspect state.stop_reason to detect this case. Illustrative fragment (not a standalone program; use the complete example linked on this page).

Building an Engine from AgentModule

AgentModule.build_engine() is a convenience factory that creates an Engine pre-bound to the agent: Illustrative fragment (not a standalone program; use the complete example linked on this page).
This is equivalent to Engine(agent=agent, ...) and is what agent.run() calls internally.

AsyncEngine

AsyncEngine provides non-blocking execution for agent workflows. It wraps the same Engine loop but runs blocking calls in a thread pool, making it safe to use inside asyncio event loops. Illustrative fragment (not a standalone program; use the complete example linked on this page).

AsyncEngine.arun(task)

Run the agent loop asynchronously. Returns the same EngineResult as Engine.run(). Illustrative fragment (not a standalone program; use the complete example linked on this page).

AsyncEngine.arun_stream(task)

Run the agent loop and yield structured EngineEvent objects as they occur — ideal for real-time UI updates or streaming progress to a client. Illustrative fragment (not a standalone program; use the complete example linked on this page).
Events are emitted at step boundaries (step_start, step_end), phase transitions (decide, act, reduce, critic, check_stop), and multi-agent events (handoff, delegate, fanout). The stream always begins with run_start and ends with run_end.

EngineEvent

Illustrative fragment (not a standalone program; use the complete example linked on this page).

EventStream

EventStream is the async queue that underpins arun_stream(). You can also use it standalone to fan out events to multiple consumers: Illustrative fragment (not a standalone program; use the complete example linked on this page).

Async models

When the configured model implements acall() (from AsyncModel), AsyncEngine can invoke it without blocking the event loop. Built-in async model adapters: Illustrative fragment (not a standalone program; use the complete example linked on this page).
AsyncEngine.arun() works with any model — sync models are automatically dispatched to a thread pool. Use async model adapters when you need true non-blocking I/O, e.g. in high-concurrency web servers.