Show progress and ask the person¶
Two settings connect an Agent to the outside:
| Setting | Role | Default |
|---|---|---|
reporter |
Receives progress events. Only watches, never changes the run | Terminal |
human |
Answers agent.ask_human |
Terminal |
The terminal¶
By default the Agent prints progress and asks questions in the terminal:
[turn 1] thinking
Let me look at main.py first.
tool read_file(path="main.py")
done 1.2KB
[turn 2] thinking
The bug is on line 3.
done: stopped by waiting_for_user (2 turns)
[turn N] thinkingstarts each model request. The model's text follows as it streams in.- Indented lines are tool calls and their results.
- The last line says why the run stopped and how many turns it took. When the Model has prices, it also shows the
cost, for example
done: stopped by waiting_for_user (5 turns, ~$0.42).
A subagent's lines are indented one level deeper than the tool call that started it, and start with its name. A subagent of a subagent goes one level deeper again. A subagent's streamed text is not shown:
tool researcher(prompt="How does Redis evict keys?")
researcher started
[researcher turn 1] thinking
tool web_search(query="Redis eviction")
done 4.0KB
researcher done: stopped by finish (2 turns)
done 612B
Other last lines:
done: reached limit(50), the task may be unfinished (50 turns)
done: error TimeoutError: took too long (3 turns)
done: interrupted by user (3 turns)
| To | Use |
|---|---|
| Hide the model's streamed text, keep the other lines | Agent(..., reporter=Terminal(show_text=False)) |
| Show nothing | Agent(..., reporter=None) |
| Allow no questions | Agent(..., human=None). ask_human then raises NoHumanError |
Write a Reporter¶
Subclass Reporter and override only the events you need. Every event does nothing by default. Pass it with reporter=.
import logging
from alpineagents import Agent, Reporter
log = logging.getLogger("agent")
class LogReporter(Reporter):
def on_tool_start(self, state, call):
log.info("turn %d: %s(%s)", state.turn, call.name, call.args)
def on_run_end(self, state, error):
log.info("%s after %d turns", state.stopped, state.turn)
agent = Agent(model="claude-sonnet-5", reporter=LogReporter())
| Event | When |
|---|---|
on_run_start(state) |
run starts |
on_think_start(state) |
Right before think or ask sends a request |
on_text(state, chunk) |
Each piece of model text as it streams in |
on_think_end(state, reply) |
Right after the reply ends |
on_tool_start(state, call) |
Right before a tool runs |
on_tool_end(state, call, result, outcome) |
Right after a tool call ends. outcome.kind says how: "done", "error", "denied", ... result is a string, or a tuple of text and images for a tool that returned an Image (result_text(result) makes it one string) |
on_context_change(state, change) |
The messages were compacted (state.compact or agent.compact), cleared (clear_tool_results) or restored (state.restore). change.kind says which |
on_model_event(state, event) |
The Model reported something outside the reply, such as a fallback |
on_run_end(state, error) |
run ends, always. error is None on a normal finish |
- Events can come from several threads. For example, Agents in different threads share the default Terminal. Make the Reporter thread-safe.
- A call a permission refused gets
on_tool_endwithouton_tool_start, withoutcome.kind"denied", or"cancelled"for the other calls of a turn it stopped.outcome.decided_bynames the permission. - A subagent with no
reporter=reports to the Reporter of the run above it. Its events come with the subagent's State, sostate.parent_id(the parent State's id, orNone) tells them apart from the top-level run's. Use subagents has a Reporter that prints a tree. - Exceptions raised in a Reporter propagate.
Write a Human¶
Subclass Human and implement ask:
from pathlib import Path
from typing import Literal, get_args, get_origin
from alpineagents import Agent, Human, tool
from alpineagents.permissions import DecideByHuman
@tool(open_world=False)
def write_file(path: str, content: str) -> None:
"""Create a file, or replace its content"""
Path(path).write_text(content)
class Unattended(Human):
"""For runs nobody watches. Says no to every yes/no question."""
def ask(self, state, prompt, returns=str):
if returns is bool:
return False
if get_origin(returns) is Literal and "no" in get_args(returns):
return "no"
raise RuntimeError(f"Nobody can answer this: {prompt}")
agent = Agent(
model="claude-sonnet-5",
tools=[write_file],
permissions=[DecideByHuman()],
human=Unattended(),
)
print(agent.run("Write a short README for this folder"))
returnsisstr,boolor aLiteral[...]of choices. Return a value of that type.- A subagent with no
human=asks the person of the run above it. - For a person reached asynchronously, such as on a web page, implement
async def aask(...)instead and useagent.aask_human.