Skip to content

Reporter, Human, Terminal

Guide: Progress and questions.

Reporter

Shows progress. Every method does nothing by default, so a subclass overrides only the ones it needs.

Pass one as Agent(reporter=...); reporter=None shows nothing. A Reporter only watches: it does not change the State or the flow. Exceptions it raises are not swallowed.

Methods may be called from several threads at once (a turn's tools run in parallel), so an implementation must be thread-safe.

Example
class Log(Reporter):
    def on_tool_start(self, state, call):
        logging.info("turn %d: %s", state.turn, call.name)

on_run_start

on_run_start(state: State) -> None

When run() starts.

on_think_start

on_think_start(state: State) -> None

Right before a request to the model, from think and ask. In think, state.turn is already this turn's number.

on_text

on_text(state: State, chunk: str) -> None

As model text arrives, chunk by chunk.

on_think_end

on_think_end(state: State, reply: Reply) -> None

Right after the reply ends. In think the reply is already recorded; ask records nothing to the context but calls this too.

on_tool_start

on_tool_start(state: State, call: ToolCall) -> None

Right before a tool runs. Every call that gets this gets exactly one on_tool_end.

on_tool_end

on_tool_end(state: State, call: ToolCall, result: str, outcome: ToolOutcome) -> None

Right after a tool call ends. A call closed by state.deny gets this without on_tool_start.

Parameters:

Name Type Description Default
state State

The State.

required
call ToolCall

The tool call.

required
result str

The string sent to the model. For a denied call, the denial reason.

required
outcome ToolOutcome

How the call ended. Branch on outcome.kind instead of reading result, because result strings are written for the model and may change.

required

on_context_change

on_context_change(state: State, change: ContextChange) -> None

When the context changes: compaction, start_from, clear_tool_results, or a rollback after think fails.

on_model_event

on_model_event(state: State, event: ModelEvent) -> None

When the Model reports something outside the reply, such as falling back to another model. Called while waiting for the reply.

on_run_end

on_run_end(state: State, error: BaseException | None) -> None

When run() ends, always, even after an exception.

Parameters:

Name Type Description Default
state State

The State. state.stopped_by says why the loop stopped.

required
error BaseException | None

The exception that ended the run, or None on a normal finish.

required

Human

Bases: ABC

Asks a person and gets an answer. Used by agent.ask_human.

A subclass implements ask, or async aask for a person reached asynchronously (a web page, a chat app), or both. An implementation asks again when an answer does not fit the returns format, asks one question at a time, and does not change the State.

Raises:

Type Description
TypeError

When creating an instance of a subclass that implements neither ask nor aask.

Example
class WebHuman(Human):
    async def aask(self, state, prompt, returns=str):
        return parse_answer(await ask_on_the_page(prompt), returns)

ask

ask(state: State, prompt: str, returns: Any = str) -> Any

Asks a person and returns the answer in the returns format, asking again when it does not fit.

Parameters:

Name Type Description Default
state State

The State of the run that asks.

required
prompt str

The question.

required
returns Any

str, bool or Literal[...].

str

Raises:

Type Description
TypeError

The Human implements only aask. Ask it with await agent.aask_human(...).

aask async

aask(state: State, prompt: str, returns: Any = str) -> Any

The async version of ask, used by agent.aask_human. By default it runs ask on a worker thread, so a blocking input() does not block the event loop.

Terminal

Terminal(*, output: TextIO | None = None, input: Callable[[str], str] | None = None, show_text: bool = True)

Bases: Reporter, Human

Shows progress in the terminal and asks the human in the terminal. It is the default reporter and human of every Agent.

Output from several threads never interleaves, and while a question is being written, other output waits.

Example output::

[turn 1] thinking
Let me look at the repo layout first.
  tool read_file(path="main.py")
  done 1.2KB
[turn 2] thinking
  context compacted: 121k → 18k tokens (cache rebuilds)
done: is_answered (5 turns, ~$0.42, cache hit 84%)

Parameters:

Name Type Description Default
output TextIO | None

Where to write. None writes to sys.stdout, looked up on every write.

None
input Callable[[str], str] | None

The function that reads an answer. It gets the full prompt text and shows it itself. None uses builtins.input, and Terminal writes the prompt to output.

None
show_text bool

False hides the model's streamed text and shows only the structure lines.

True

show_text instance-attribute

show_text = show_text

Whether the model's streamed text is shown. Can be changed between runs.

on_run_start

on_run_start(state: State) -> None

Prints nothing.

on_think_start

on_think_start(state: State) -> None

Writes [turn N] thinking.

on_text

on_text(state: State, chunk: str) -> None

Writes the model's text as it arrives, unless show_text is False.

on_think_end

on_think_end(state: State, reply: Reply) -> None

Ends the line if the text stopped mid-line.

on_tool_start

on_tool_start(state: State, call: ToolCall) -> None

Writes tool read_file(path="main.py").

on_tool_end

on_tool_end(state: State, call: ToolCall, result: str, outcome: ToolOutcome) -> None

Writes one line by outcome.kind: done 1.2KB (the result size), error {name}: ..., aborted {name}: ..., denied {name}: ..., or the input error itself.

on_context_change

on_context_change(state: State, change: ContextChange) -> None

Writes context compacted: 121k → 18k tokens (cache rebuilds) and similar lines, under the next turn header.

on_model_event

on_model_event(state: State, event: ModelEvent) -> None

Writes model: {event.message}.

on_run_end

on_run_end(state: State, error: BaseException | None) -> None

Writes one last line, such as done: is_answered (5 turns, ~$0.42, cache hit 84%), done: reached limit(50), the task may be unfinished (50 turns) or done: interrupted by user (3 turns).

ask

ask(state: State, prompt: str, returns: Any = str) -> Any

Asks in the terminal and returns the answer in the returns format. Choices are shown after the question (yes/no, a/b/c), and an answer that does not fit gets a hint and the question again. Concurrent questions wait in line.

Raises:

Type Description
EOFError

The input ended.

KeyboardInterrupt

The person pressed Ctrl+C.