Turns¶
A turn is a unit of work: which tool to run with what arguments. Turns describe what should happen; tools define how.
Creating and running¶
You can pass the tool by name (string) or by reference (the callable). The turn resolves the tool from ToolRegistry at construction time.
UnregisteredToolError
Turn(name, ...) raises UnregisteredToolError if no tool with that name is found in ToolRegistry. Tools must be decorated before any turn references them.
from pygents import Turn
# single value (tool by name)
turn = Turn("fetch", kwargs={"url": "https://example.com"}, timeout=30)
result = await turn.returning()
# by callable
turn = Turn(fetch, kwargs={"url": "https://example.com"}, timeout=30)
# with positional args
turn = Turn("fetch", args=["https://example.com"], timeout=30)
result = await turn.returning()
# streaming
turn = Turn("stream_lines", kwargs={"path": "/tmp/data.txt"})
async for line in turn.yielding():
print(line)
returning()— for coroutine tools, returns the tool's resultyielding()— for async generator tools, yields each value
The agent's run() picks the right method automatically.
WrongRunMethodError
Using returning() on an async generator tool or yielding() on a coroutine tool raises WrongRunMethodError.
Attributes¶
| Attribute | Set | Mutable while running? |
|---|---|---|
tool, args, kwargs, timeout |
init | No |
tags |
init | No. A frozenset[str] of labels used to filter global @hook declarations. Empty by default. See Hooks — Tag filtering. |
output |
by framework after run | Yes. The return value for coroutine tools, or a list of all yielded values for async generator tools. None on a fresh turn. |
metadata |
by framework during run | Yes. A TurnMetadata dataclass with three fields: start_time, end_time, stop_reason. All default to None on a fresh turn. |
Access execution results via the metadata object:
turn.output # the tool's return value (or list for generators)
turn.metadata.start_time # datetime set when the turn started
turn.metadata.end_time # datetime set when the turn finished
turn.metadata.stop_reason # StopReason enum value
metadata fields are not constructor parameters — they are managed by the framework and set during execution. from_dict() restores them directly.
SafeExecutionError
Changing immutable attributes while running raises SafeExecutionError. Calling returning() or yielding() on an already-running turn also raises SafeExecutionError.
Why block reentrancy?
A turn cannot run twice simultaneously. Most attributes are immutable while running. This prevents race conditions from accidental reuse — if you need to run the same work again, create a new turn.
Timeouts¶
Every turn has a timeout (default: 60 seconds). This prevents unbounded execution — a stuck network call or infinite loop won't block the agent forever.
For returning(), the timeout applies to the single await. For yielding(), it applies to the entire run including all yielded values.
turn.metadata.stop_reason is a StopReason enum (importable from pygents):
| Outcome | stop_reason |
|---|---|
| Success | StopReason.COMPLETED |
| Timeout | StopReason.TIMEOUT |
| Error | StopReason.ERROR |
| Cancelled | StopReason.CANCELLED |
TurnTimeoutError
When a turn exceeds its timeout, TurnTimeoutError is raised and turn.metadata.stop_reason is set to TIMEOUT.
Dynamic args and kwargs¶
Callable positional args and kwargs are late-evaluated: any no-arg callable passed as an arg or kwarg is called at tool invocation time, not at turn creation. This supports dynamic config, rotating tokens, memory reads, or any value that should be fresh when the tool actually runs.
turn = Turn(
"fetch",
args=[lambda: get_current_url()], # called when tool runs, not now
kwargs={
"token": lambda: get_current_token(), # called when tool runs, not now
},
)
This is especially useful when turns are queued — the lambda captures the latest state when the turn executes, not when it was created.
The sections below cover advanced configuration. If you're getting started, continue to Agents.
Hooks¶
Turn hooks fire at specific points during execution. Hooks are stored as a list and selected by type at run time. Exceptions in hooks propagate.
| Hook | When | Args |
|---|---|---|
BEFORE_RUN |
Before tool runs (after lock acquired) | (turn) |
AFTER_RUN |
After successful completion | (turn, output) |
ON_TIMEOUT |
Turn timed out | (turn) |
ON_ERROR |
Tool or hook raised (non-timeout) | (turn, exception) |
ON_COMPLETE |
Always fires in finally block (clean, error, or timeout) | (turn, stop_reason) |
Attach hooks after construction via method decorators or turn.hooks.append(h):
from pygents import Turn, hook, TurnHook
turn = Turn("my_tool", kwargs={})
@turn.before_run
async def log_start(turn):
print(f"Starting {turn.tool.metadata.name}")
# Or, for global hooks that fire for every turn:
@hook(TurnHook.BEFORE_RUN)
async def log_all_turns(turn):
print(f"Starting {turn.tool.metadata.name}")
Hooks are registered in HookRegistry at decoration time. Use named functions so they serialize by name.
ValueError
Registering a different hook with a name already in use in HookRegistry raises ValueError. Re-registering the same hook under the same name is allowed.
Serialization¶
data = turn.to_dict() # dict with tool_name, args, kwargs, metadata, hooks, ...
turn = Turn.from_dict(data) # restores from dict, resolves tool and hooks from registries
Datetimes are ISO strings. Hooks are serialized by name and resolved from HookRegistry on deserialization.
UnregisteredHookError
Turn.from_dict() raises UnregisteredHookError if a hook name is not found in HookRegistry.
Errors¶
| Exception | When |
|---|---|
SafeExecutionError |
Changing immutable attributes or calling returning()/yielding() while running |
WrongRunMethodError |
returning() on async generator or yielding() on coroutine |
TurnTimeoutError |
Turn exceeds its timeout |
UnregisteredToolError |
Tool name not found in ToolRegistry at construction |
UnregisteredHookError |
Hook name not found in HookRegistry during from_dict() |
ValueError |
Duplicate hook name in HookRegistry |