Agent¶
Learn: Your first agent. Concepts: Agent.
Agent
¶
Agent(model: str | Model, *, system: str | None = None, tools: Iterable[Any] = (), skills: Iterable[str] = (), loop: Any = None, reporter: Reporter | None = DEFAULT, human: Human | None = DEFAULT, store: Store | None = None, permissions: Iterable[Permission] | None = None, name: str | None = None, description: str | None = None)
An agent's settings, and the actions that need a model, tools or a human.
Settings do not change after creation. Use copy to get an Agent with different settings.
The history of a run lives in a State, and the order of steps is decided by the loop.
Example
agent = Agent(model="claude-sonnet-5", system="You are a coding assistant", tools=[read_file])
answer = agent.run("Find the bug in main.py")
Creates an Agent. Mistakes in the settings are reported here, with how to fix them.
Creating an Agent uses no network and needs no API key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str | Model
|
A model name such as |
required |
system
|
str | None
|
The system prompt. |
None
|
tools
|
Iterable[Any]
|
|
()
|
skills
|
Iterable[str]
|
Not implemented yet. A non-empty value raises |
()
|
loop
|
Any
|
The loop |
None
|
reporter
|
Reporter | None
|
Receives progress notifications. Defaults to the shared |
DEFAULT
|
human
|
Human | None
|
Answers |
DEFAULT
|
store
|
Store | None
|
Saves the States this Agent runs as they go, so they can be continued with
|
None
|
permissions
|
Iterable[Permission] | None
|
Permissions that decide, before a tool call runs, whether it may run (see
|
None
|
name
|
str | None
|
The Agent's name. |
None
|
description
|
str | None
|
What the Agent does. |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
A setting has the wrong type, for example a function without |
ValueError
|
Two tools share a name, an Agent in |
model
property
¶
model: Model
The Model object. A model string given to Agent(model=...) is already turned into one.
tools
property
¶
tools: tuple[Any, ...]
The items passed in tools=, as given. tool_map has the tools they stand for, by name.
tool_map
property
¶
Every tool the model can call, by the name it calls it: @tool functions, the @tool methods of
objects in tools=, and the tools of MCP servers (MCPTool, named {server}__{tool}).
Use it to look up the tool of a call, and read what the tool does from its hints (read_only,
destructive, idempotent, open_world). Read-only: to change the tools, use copy(tools=...).
MCP servers' tools are listed only while the servers are connected: during a run, or inside with agent:.
Looking up a missing name before that raises a KeyError that says so.
Example
for call in state.pending_calls:
tool = agent.tool_map.get(call.name) # None for a name the model made up
if tool is not None and not tool.read_only:
print(f"{call.name} may change something")
reporter
property
¶
reporter: Reporter | None
The Reporter that receives progress notifications, or None. Defaults to the shared Terminal. This
is the Agent's own setting: a subagent that was not given one reports to the Reporter of the run that
started it (see Agent(reporter=)), and this property does not show that.
human
property
¶
human: Human | None
The Human that answers ask_human, or None. Defaults to the shared Terminal. This is the
Agent's own setting: a subagent that was not given one asks the Human of the run that started it (see
Agent(human=)), and this property does not show that.
store
property
¶
store: Store | None
The Store that saves this Agent's States as they run, or None. This is the Agent's own setting: a
subagent without one is saved in the Store of the run that started it, and this property does not show
that.
permissions
property
¶
permissions: tuple[Permission, ...]
The permissions that decide whether each tool call may run, in the order given. Empty both without
permissions= (every call runs unchecked) and with an empty list (every call is denied).
run
¶
run(prompt_or_state: str | State) -> Any
Runs the loop on a prompt or a State and returns what the loop returns, usually the answer.
A prompt string starts a new State with that one user message. Any Agent can continue any State, so
agent.copy(model=...) can carry on where another model left off. A State is run by one run or
arun at a time.
MCP servers in tools= connect for the run and disconnect after it, unless with agent: keeps
them open. If an exception leaves the run, it is recorded in state.history, tool calls still
waiting for a result are closed ((interrupted by user) after Ctrl+C), and the exception is
raised as is. After Ctrl+C you can call run again with the same State.
Closed calls get (interrupted by user) after Ctrl+C or cancellation and (aborted: TimeoutError)
(the exception's type) after another exception, SystemExit included (so turning SIGTERM into
SystemExit ends a run cleanly). A sync tool still running keeps running in its thread; if it finishes
later, the model gets its result as a notice at the next think. Afterwards state.stopped is
None.
Continuing a State that already ran: if its last run stopped at an until function that is still true,
the loop stops at once (add a message first). If it stopped at limit, it runs up to limit more
turns, because each call counts turns from zero. A State ended with finish() raises ValueError.
With store=, the State is saved as the run goes: at the start and end of the run, before and after
each think, after each tool result, and after use_tools and compact. What permission checks
recorded (denied calls, the person's answers) is saved before any tool of the turn runs. A save also
happens after an exception. If saving fails during a run that goes well, the run raises the store's
exception. If the run is already failing, the original exception is raised with a note about the failed
save, so except RateLimitError: still catches it; Ctrl+C or cancellation while saving is raised
instead of the original. A failed save after one tool result does not stop the other tools: the save at
the end of use_tools writes what is missing, or raises. Nothing that failed to save is lost: the next
save writes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt_or_state
|
str | State
|
A prompt string, which starts a new |
required |
Returns:
| Type | Description |
|---|---|
Any
|
What the loop returns. The default loop returns |
Raises:
| Type | Description |
|---|---|
TypeError
|
The loop is async (use |
NoHumanError
|
A |
ValueError
|
The prompt is empty, the State was already ended with |
TypeError
|
Also when called inside an async run, where it would block the event loop (use |
Warns:
| Type | Description |
|---|---|
ResumeWarning
|
The State was loaded from a store, and it was last run by an Agent with a different
name, model, system prompt, tools or MCP servers. Only the first run after |
Example
state = State(messages=[Message.user("Find the bug in this repo")])
answer = agent.run(state)
print(state.stopped, state.usage.cost)
arun
async
¶
arun(prompt_or_state: str | State) -> Any
The async version of run.
Uses this Agent's loop if it is async, or adefault_loop if the Agent uses the default loop.
Cancelling the task follows the same rules as Ctrl+C in run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt_or_state
|
str | State
|
A prompt string, which starts a new |
required |
Returns:
| Type | Description |
|---|---|
Any
|
What the loop returns. The default loop returns |
Raises:
| Type | Description |
|---|---|
TypeError
|
The loop is a sync loop other than |
ValueError
|
The prompt is empty, the State was already ended with |
NoHumanError
|
A |
Warns:
| Type | Description |
|---|---|
ResumeWarning
|
The State was loaded from a store, and it was last run by an Agent with a different
name, model, system prompt, tools or MCP servers. Only the first run after |
think
¶
think(state: State, tools: Iterable[Any] | None = None) -> None
Sends the messages to the model once and records its reply in the State.
The reply may ask for tool calls. Check state.pending_calls and run them with use_tools.
If the model call fails, the request is taken back (state.messages and state.turn are as they
were before this call) and the error is raised and recorded in the history. Once the reply is recorded,
nothing undoes it.
Any Agent can think on any State, so a State can be continued with another model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State to continue. |
required |
tools
|
Iterable[Any] | None
|
Which of this Agent's tools the model may see this turn. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
The State is finished, has no messages, still has tool calls waiting for results, is
already waiting on a model, or |
TypeError
|
Called inside an async run, where it would block the event loop (use |
ProviderError
|
The model provider failed ( |
athink
async
¶
athink(state: State, tools: Iterable[Any] | None = None) -> None
The async version of think. Cancelling it takes the request back like any other failure.
use_tools
¶
use_tools(state: State) -> None
Runs the tool calls from the last reply and records their results in the State.
Calls to tools made with parallel=True (the default) run at the same time, then the rest run one
by one. Does nothing if there are no calls. A tool that raises does not become an error result:
results of the calls that finished are recorded, and the exception is raised as is, with a note naming
the call. The calls that did not finish stay pending, so the next think raises ValueError: call
use_tools again (calls that already have a result do not run again) or let the exception leave the
run, which closes them. Invalid arguments and unknown tool names go back to the model as
(input error: ...) results.
The calls run in this order: tools with parallel=True on worker threads at the same time, then tools
with parallel=False one at a time in the order the model asked. The results go into
state.messages together, in the order the model asked, once the last call has one.
With permissions=, every call is checked before any tool runs. A denied call does not run, and the
model gets the reason as its error result. A denial with stop=True cancels the other calls of the
turn too, so no tool of the turn runs, and the loop stops before its next turn. Called on the State of a
subagent's run by an Agent that is not that run's own (a custom loop calling other.use_tools(state)), the
call is checked by the DenyPermission of the runs above the subagent first, then by this Agent's list.
Every call ends as one ToolResultEntry whose outcome says how: done, error, input_error, aborted,
interrupted, denied or cancelled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State whose |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
The State is finished. |
TypeError
|
Called inside an async run, where it would block the event loop (use |
ause_tools
async
¶
ause_tools(state: State) -> None
The async version of use_tools, with the same checks: inside a subagent's run, an Agent that is not
that run's own is checked by the DenyPermission of the runs above first.
async def tools run as tasks on the running event loop, other tools on worker threads.
Cancelling it follows the same rules as Ctrl+C.
ask
¶
ask(state: State, prompt: str, returns: Any = str, *, retries: int = 2) -> Any
Asks the model a question about the current context and returns the answer in the returns format.
The question and answer are recorded in state.history but do not enter the context, so the run
continues as if the question was never asked. The model cannot call tools while answering. Any Agent
can ask about a State, not only the one running it. It adds nothing to state.turn and writes no
model_request entry, but its tokens count in state.usage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State whose context the model reads. |
required |
prompt
|
str
|
The question. |
required |
returns
|
Any
|
The answer format: |
str
|
retries
|
int
|
How many more times to ask when the answer does not fit |
2
|
Returns:
| Type | Description |
|---|---|
Any
|
The answer, converted to |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
The State is finished (so do not combine |
OutputError
|
The answer still did not fit |
ProviderError
|
The model provider failed. |
Example
@dataclass
class Review:
approved: bool
reason: str
review = agent.ask(state, "Is this change safe to merge?", returns=Review)
aask
async
¶
aask(state: State, prompt: str, returns: Any = str, *, retries: int = 2) -> Any
The async version of ask.
ask_human
¶
ask_human(state: State, prompt: str, returns: Any = str) -> Any
Asks the person through human and returns the answer in the returns format.
The question and answer are recorded in state.history. The context does not change.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State of the current run. |
required |
prompt
|
str
|
The question. |
required |
returns
|
Any
|
The answer format: |
str
|
Returns:
| Type | Description |
|---|---|
Any
|
The answer, converted to |
Raises:
| Type | Description |
|---|---|
NoHumanError
|
The Agent was created with |
TypeError
|
|
Example
choice = agent.ask_human(state, "Run the command?", returns=Literal["yes", "no", "always"])
aask_human
async
¶
aask_human(state: State, prompt: str, returns: Any = str) -> Any
The async version of ask_human.
Awaits human.aask when the Human has it, and otherwise runs human.ask on a worker thread.
compact
¶
compact(state: State, instructions: str | None = None) -> None
Asks the model to summarize the messages, then replaces them with the first user message and that summary.
It is the same change as state.compact(summary) with the model's summary, and the usage of the
request is added to state.usage. Like ask, it writes no model_request entry, so the model that
wrote a summary is not in history. Messages added while the model was writing the summary stay after it.
state.history keeps everything. If anything fails, the messages are left unchanged.
In a loop, compact_if_full calls this only when the context is getting full.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State to compact. |
required |
instructions
|
str | None
|
What the summary should keep, for example |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
The State still has tool calls waiting for results, or another compaction of it is under way. |
TypeError
|
Called inside an async run, where it would block the event loop (use |
OutputError
|
The model returned an empty summary. |
ProviderError
|
The model provider failed. |
acompact
async
¶
acompact(state: State, instructions: str | None = None) -> None
The async version of compact.
context_tokens
¶
context_tokens(state: State) -> int
Estimated size of what the model would receive next, in tokens: the State's messages plus this Agent's system prompt and tool definitions.
An estimate: it starts from the token count the provider reported for the last reply and adds a character-count guess for the messages after it. Each image, in a user message or a tool result, counts as 1,600 tokens.
Example
print(f"{agent.context_tokens(state):,} tokens")
context_used
¶
context_used(state: State) -> float
The fraction of this Agent's model's context window that context_tokens(state) fills. Can go above
1.0. compact_if_full compares it with its at.
Example
if agent.context_used(state) > 0.8:
agent.compact(state)
copy
¶
copy(**changes: Any) -> Agent
Returns a new Agent with some settings changed. The original Agent does not change.
The new Agent goes through the same checks as Agent(...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**changes
|
Any
|
Settings to change, by the same names as the |
{}
|
Returns:
| Type | Description |
|---|---|
Agent
|
The new Agent. |
Raises:
| Type | Description |
|---|---|
TypeError
|
A name in |
Example
quiet = agent.copy(model=FakeModel(["Done"]), reporter=None)