Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Context

AgentContext is the ctx passed to every agent. It extends the workflow context, so all the durable primitives are available alongside agent-specific operations.

These behave exactly as they do in a workflow. Use them for anything non-deterministic or side-effecting:

@step, ctx.step, ctx.now, ctx.uuid, ctx.random, ctx.emit, ctx.emit_status, ctx.sleep, ctx.sleep_until, ctx.wait_for_signal, ctx.interrupt, ctx.checkpoint.

Use @step for named application functions and ctx.step for runtime-generated step IDs. See Timers, Signals, and Human-in-the-Loop.

A tool-loop turn that runs for a very long time accumulates an event log like any other run. ctx.continue_as_new(params=...) ends the current run and starts a fresh successor run in the same thread, carrying forward whatever you pack into params. It’s the same primitive workflows use to bound history growth. Downstream consumers (run(), thread turn history, projections) follow the chain transparently, so the handoff presents as one logical turn backed by several physical runs.

ctx.continue_suggested turns True once the run’s event log has consumed 80% or more of max_event_count_per_run, so you hand off at a clean tool-loop boundary instead of hitting the hard cap:

if ctx.continue_suggested:
# roll the conversation into a compact summary and start a fresh run
await ctx.continue_as_new(params={"summary": running_summary})

Kafka-driven and branch/stream (non-durable) executions reject continue_as_new with a typed ContinueAsNewNotSupportedError. A consumed record or an in-memory branch replay is its own unit of work, so there’s no “successor run” to hand off to.

call_agent runs another agent and returns its result. It records like any other step, so the child isn’t re-run on replay.

summary = await ctx.call_agent(summarizer, text=document, id="summarize")

gather_subagents runs several agent calls concurrently and waits for all of them. (ctx.gather is the workflow primitive for plain awaitables.)

results = await ctx.gather_subagents(
(classifier, {"text": a}),
(classifier, {"text": b}),
id="classify-batch",
)

AgentLimits on the parent caps depth and parallelism so a fan-out can’t run away.

An agent can hand off to the durable engine directly. spawn_workflow starts a workflow run; wait_for_workflow blocks on its result.

run_id = await ctx.spawn_workflow("nightly_report", params={"date": today})
result = await ctx.wait_for_workflow("nightly_report", run_id)

ctx.memory is cross-turn state for an agent, keyed by a namespace tuple so you can scope it per user or per thread:

# remember a preference this turn, recall it on a later one
await ctx.memory.put(("user", user_id), "preferences", {"tone": "concise"})
prefs = await ctx.memory.get(("user", user_id), "preferences") # {"tone": "concise"} or None

ctx.thread is the conversation thread the turn belongs to, when there is one. Both let an agent carry context across separate turns without you wiring up storage by hand.

ctx.turn_id identifies the current turn and can be used in logs and idempotency keys.