Runner

The per-run coordinator for provider calls, tools, approvals, handoffs, and output.

Runner

A Runner owns one execution. Application code rarely constructs it directly — engine.execute() creates it, injects shared services, and returns the result.

Think of the runner as the object you would want in a debugger: every transition for this run lives here.

Why it exists#

Agent execution is stateful. If provider calls, tool calls, approvals, and handoffs are coordinated by separate application paths, you cannot reconstruct the run later. The runner keeps those transitions ordered and attributable to a single runId.

The runner answers: what happened in this execution, and in what order?

How you use it#

const result = await engine.execute(agent, "Should we refund order ord_7741?", {
  sessionId: "refund_441",
});

console.log(result.runId);
console.log(result.output);

RunResult includes the run id, output, messages, finish reason, and run context. Observability comes from the engine’s event / trace pipeline — subscribe to events or export traces rather than expecting a hidden result.trace field.

What it coordinates#

run.started
  → session / memory load
  → provider.call
  → tool.execute  (optional approval pause)
  → handoff       (optional)
  → provider.call
  → output.validated
run.completed | run.failed

Exact stage order is documented in Execution Pipeline.

Runner vs Engine#

EngineRunner
LifetimeProcessOne run
CreatesRunners
OwnsShared servicesExecution state
ScaleOne (typically)Many concurrent

See Mental Model.

Observing a run#

Attach an event bus or TraceManager to the engine before executing. Studio and exporters consume those events:

import { JsonTraceExporter, TraceManager } from "@shiro-sdk/core";

const traces = new TraceManager();
const engine = new Engine({ events: traces });

await engine.execute(agent, input);
const json = await traces.export(new JsonTraceExporter());

Open the export in Studio, or keep it for a bug report. See Tracing.

Best practices#

  • Let the engine create runners
  • Pass sessionId when the conversation continues
  • Treat approval rejection as a normal outcome, not a mysterious crash
  • Keep mutating tools idempotent so a retried runner step is safe

Common mistakes#

  • Reimplementing the loop with manual provider + tool calls
  • Continuing a paused approval outside the runtime
  • Mixing unstructured application logs with “the run failed” and calling it done
  • Creating runners without going through a ready engine