Running agents¶
Runner turns an Agent plus an input into one run. It is stateless — all
per-run state lives inside the loop it starts — and it exposes exactly three
entry points that differ only in how you consume the run.
from lovia import Runner
result = await Runner.run(agent, "Draft a release note.") # run to completion
result = Runner.run_sync(agent, "Summarize this file.") # scripts / REPLs
handle = Runner.stream(agent, "Explain compaction.") # events as they happen
agent.run(...) / agent.run_sync(...) / agent.stream(...) are the same
calls, spelled as instance methods.
The three entry points¶
Runner.run(agent, input, **options) -> RunResult — awaits the run and
returns the final result. Failures raise (GuardrailTripped,
BudgetExceeded, ProviderError, ... — the catalog).
Runner.run_sync(...) — the same, wrapped in asyncio.run() for code
that isn't async yet. Calling it from inside a running event loop raises
UserError with the fix in the hint (use await Runner.run(...)).
Runner.stream(...) -> RunHandle — starts the run and hands back a
handle that is both async-iterable (typed events) and awaitable (the
final result):
handle = Runner.stream(agent, "Analyze these logs.")
async for ev in handle: # never raises for run failures
...
result = await handle.result() # returns the RunResult, or raises the run's error
Iteration is single-shot (a second async for raises RuntimeError) and
always ends with exactly one terminal event — RunCompleted or RunFailed.
await handle is shorthand for await handle.result(); if nothing has
iterated the stream yet, result() drives it to completion itself.
handle.cancel() requests cooperative cancellation without pre-wiring a
CancelToken, and handle.approvals is the out-of-band
approval channel.
The events themselves are catalogued in Streaming.
Run options¶
All three entry points accept the same keywords:
| Option | Default | What it does |
|---|---|---|
context |
None |
your dependency object, surfaced as ctx.deps (Agents) |
output_type |
None |
run-wide override of the agent's output type |
extra_instructions |
None |
per-run system-prompt addendum, rendered after the agent's own instructions (and re-applied to every agent a handoff reaches) |
max_turns |
50 |
hard cap on model turns; exceeding it raises MaxTurnsExceeded |
budget |
None |
a RunBudget limiting what the run may spend (Budgets) |
cancel_token |
None |
pre-wired cooperative cancellation (Cancellation) |
mailbox |
None |
inbound steering channel (Steering) |
retry |
agent's | per-call override of the provider retry posture |
context_policy |
agent's | per-call override of the context policy |
session + session_id |
None |
conversation persistence (Sessions & checkpoints) |
checkpoint |
None |
crash recovery and idempotent runs (Sessions & checkpoints) |
tracer |
None |
run-scoped tracing (Observability) |
retry and context_policy are the two posture overrides — they default
to the agent's own configuration, and the initial agent's posture
governs the whole run even across handoffs. The rest are limits and
wiring, which deliberately have no agent-side counterpart.
Inputs¶
input is either a string (one user message) or a list of Message values
for multi-message openings:
from lovia import Runner, system, user
result = await Runner.run(
agent,
[
system("Answer as a pirate."), # extra system message, kept in the transcript
user("Where do we sail?"),
],
)
Images and files¶
Message content can be a list of typed parts instead of a string —
TextPart, ImagePart, FilePart — and providers translate them to their
wire format:
from lovia import ImagePart, Runner, TextPart, user
result = await Runner.run(
agent,
[
user(
[
TextPart("What's in this screenshot?"),
ImagePart.from_path("shot.png"),
]
)
],
)
(user(...) also accepts a plain string, or a single part; a part list
must contain typed parts — a bare str inside the list is not coerced.)
ImagePart(url=...)orImagePart(data=..., mime_type=...)— exactly one ofurl/data, and base64datarequiresmime_type.ImagePart.from_path()loads and encodes a local file, inferring the mime type from the suffix. Optionaldetail="low"|"high"|"auto".FilePart— same shape plusfilename; constructorsfrom_path,from_bytes,from_base64,from_url. URL parts are provider-native references — lovia never downloads them.
The result¶
RunResult field |
What it is |
|---|---|
output |
the final answer — str, or a validated instance of the run's output_type |
entries |
this run's own transcript: its input plus everything it produced, across handoffs |
messages |
chat-format view derived from entries (lossy) |
final_agent |
the agent that produced the output (differs from the initial one after a handoff) |
usage |
cumulative tokens — input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens; agent-as-tool sub-runs included |
turns |
how many model turns the run took |
finish_reason |
the final turn's provider-reported reason — check "stop" vs "length" to detect a max_tokens-truncated answer |
entries deliberately excludes the system prompt and prior session history,
so it is identical whether the run finished fresh or was rebuilt from a
checkpoint. For the full conversation, read ctx.entries inside a hook, or
session.load() after the run.
Sharp edges¶
RunResult.entriesis not the whole transcript — it's the run's delta. Code that renders "the conversation" from it will silently drop prior history; use the session for that.finish_reasoncan beNone— when the provider reported none, or when the result was replayed from a completed checkpoint (it isn't persisted in snapshots).- A model reply with no content and no tool calls completes the run with
an empty-string output (logged as a warning). It's almost always a
provider hiccup or
max_tokenstruncation — checkfinish_reasonbefore trusting an empty answer. run_syncowns the event loop — it refuses to run inside one. In notebooks with a live loop, useawait Runner.run(...).
See also¶
- Streaming — the event catalog behind
Runner.stream - Sessions & checkpoints — persistence options
- Production controls — budgets, cancellation, steering, and retries
- Examples:
01_hello.py,06_multimodal.py