Skip to content

Reporter, Human, Terminal

Guide: Show progress and ask the person.

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. Agents in different threads share the default Terminal.

A Reporter gets the State and the call, not the Agent. To read a tool's hints in on_tool_start, keep a reference to the Agent yourself (reporter.agent = agent after creating both) and look the tool up in agent.tool_map.

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: ToolResultContent, outcome: ToolOutcome) -> None

Right after a tool call ends. A call a permission denied or cancelled did not run, so it gets this without on_tool_start.

Parameters:

Name Type Description Default
state State

The State.

required
call ToolCall

The tool call.

required
result ToolResultContent

What was sent to the model: a string, or a tuple of TextBlock and Image when the tool returned an image (result_text(result) makes it one string). 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. For a denied or cancelled call, outcome.decided_by names the permission that decided.

required

on_context_change

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

When the messages change: compact (by the model or state.compact), clear_tool_results or restore. Not for the import of State(messages=...), and not when a failed think is taken back.

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 says why the run stopped (None after an exception, or when a loop written without @loop ended on its own check).

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.

A subagent's lines are indented one level deeper per generation (below the tool line of the call that started it) and start with its Agent's name, so its run shows up under that call. Only the structure lines of a subagent are shown, not its streamed text (show_text is for the top-level run).

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)
  tool researcher(prompt="How does Redis evict keys?")
    researcher started
    [researcher turn 1] thinking
      tool web_search(query="Redis eviction")
      done 4.0KB
    [researcher turn 2] thinking
    researcher done: stopped by finish (2 turns, ~$0.01)
  done 612B
[turn 3] thinking
done: stopped by waiting_for_user (3 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. A subagent's streamed text is never shown.

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 for the top-level run. A subagent's run writes researcher started.

on_think_start

on_think_start(state: State) -> None

Writes [turn N] thinking, with the Agent's name in a subagent's header: [researcher 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 or the run is a subagent's.

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: ToolResultContent, outcome: ToolOutcome) -> None

Writes one line by outcome.kind: done 1.2KB (the result size, done 35.1KB (1 image) with images), error {name}: ..., aborted {name}: ..., denied {name}: ..., cancelled {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: stopped by waiting_for_user (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). A subagent's line starts with its name: researcher done: stopped by finish (2 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.