Skip to content

Agent Initialization

The recommended path is the Agent class itself: every part of it is a replaceable object. Use setup_agent when you just want something running quickly — it wires up the built-in tools, system prompt, session commands, and display layer in one call and returns an initialized agent.

Shortest path: setup_agent

from xun import setup_agent, interactive_session

def weather(city: str) -> str:
    """Query the weather for a given city."""
    return f"Sunny in {city}"

agent = setup_agent(tools=[weather])          # initialize() has already run
interactive_session(agent, task="What is the weather in Paris?")
def setup_agent(
    name: str = "agent",
    tools: list[Callable] = [],
    default_tools: bool = False,
    default_system_prompt: bool = True,
    default_commands: bool = True,
    display: DisplayAbstract | None = None,
    workdir: Path | str | None = None,
) -> Agent[Agent.T.Init]: ...
Parameter Default Description
name "agent" Agent name; appears in the display layer and in events
tools [] Additional custom tool functions
default_tools False Whether to load the 33 built-in tools; also enables the sub-agent derivation tools and tool-related session commands
default_system_prompt True Writes the built-in system prompt
default_commands True Registers the built-in session commands (/help, etc.)
display None None uses the console display Display(); pass NullDisplay() to stay silent in programmatic calls
workdir None Working directory; None means the current directory

Initialization happens last

setup_agent calls initialize() only after all configuration is done, so the after_initialize hook sees a fully configured agent.

Full path: the Agent class

Agent is a dataclass, and each field is a replaceable core object:

Field Default Inherited on derivation Purpose
name agent-<uuid8> New name Identifier in the display layer and in events
identifier uuid New uuid Process-unique ID; the display layer distinguishes agents by it
display NullDisplay() Shared reference Display layer, see Display Extensions
conversation Conversation() Not copied by default Message sequence and system prompt sections
toolbox ToolBox() Cloned Toolbox, see Tool Setup
command CommandRegistry() Cloned Session commands
workspace Workspace() Shares the same object Working directory + lazy temp directory
cancel_event ChainedEvent() Attached under the parent event Cancellation chain, see Chained Cancellation
config load_config().clone() Cloned Independent copy of the configuration for this agent
api_call_semaphore 3 Shared reference Process-wide ceiling on concurrent model calls
extension_loader Global loader Shared reference Extensions are replayed on every initialize()
state AgentState() Not inherited Typed state container
hooks Hooks() Not inherited Lifecycle hooks
compactor AutoCompactor() Not inherited Conversation compaction strategy
from xun import Agent, ToolBox

def weather(city: str) -> str:
    return "The weather in " + city + " is sunny."

agent = Agent(toolbox=ToolBox().register(weather)).initialize()
answer = agent.instruct("What is the weather in Paris?").execute()
print(answer.unwrap())

The chained form (as used in the README):

greeting = (
    Agent()                 # Agent[Uninit]
        .initialize()       # Agent[Init]
        .instruct("Hi~")    # Agent[Init]
        .execute()          # Result[str, ErrorInfo]
        .unwrap()           # str
)

What initialize() does

flowchart TD
    A["Agent(...)"] --> B["initialize()"]
    B --> B1["Replay extension setup"]
    B1 --> B2["Auto-detect model when the name is empty"]
    B2 --> B3["display.bind(agent)"]
    B3 --> B4["workspace.prepare()"]
    B4 --> B5["compactor.install(hooks)"]
    B5 --> B6["after_initialize hook"]
    B6 --> B7["Sync AGENTS.md"]
    B7 --> C["Agent[Init]: ready to execute"]
    C --> D["finalize()"]
    D --> D1["before_finalize hook"]
    D1 --> D2["display.unbind + AgentUnbindEvent"]
    D2 --> E["Agent[Final]: no longer usable"]

An agent must be initialized before execute(); the runtime checks this as well and raises a RuntimeError with a hint. Details of the state transitions are in Lifecycle and Type States.

Execution and return values

result = agent.instruct("Write a 100-word story").execute()      # Result[str, ErrorInfo]
story  = agent.instruct(...).execute(schema=StoryModel)          # Result[StoryModel, ErrorInfo]
agent.execute_command("help")                                    # Result[None, ErrorInfo]

execute / execute_command are wrapped by except_safe: exceptions are always converted into Result.Err(ErrorInfo), except CancelledError and KeyboardInterrupt, which are raised as-is. The Result interface:

Method Behavior
is_ok() / is_err() Decide which branch you are on
unwrap() / unwrap_err() Take the value; raises a plain Exception on the wrong branch
value Raw value (either T or E)
value_str() / value_json() String / JSON form
Result.from_payload(data, ok_factory, err_factory) Restore from JSON

max_iterations defaults to 512; exceeding it raises RuntimeError, which surfaces as Result.Err. The object passed via context= is handed to tools as ToolCallContext.value, see Tool Setup.

Deriving sub-agents

Agent.inherit(parent, ...) returns an uninitialized child agent, and lets you choose which parts to share:

child = Agent.inherit(
    parent,
    share_workspace=True,      # Same Workspace object (same workdir and lazy tempdir)
    share_display=True,        # Same display layer
    copy_toolbox=True,         # Shallow copy of the toolbox
    copy_command=True,         # Shallow copy of the command registry
    copy_conversation=False,   # Conversation is independent by default
    inherit_cancel_event=True, # Cancel event attached to the parent chain
)

config is always cloned, and state / hooks / compactor are always fresh. The child agent goes through the defaults again for both extensions and the compaction strategy; see Sub-agent Derivation for how this combines with the built-in derivation strategies.

Attaching parts manually

There is no add_tool()-style method — all configuration means attaching something to the corresponding object:

agent.toolbox.register(my_tool)              # Tool (returns the ToolBox, chainable)
agent.command.register(my_command)           # Session command
agent.hooks.before_tool_call.add(handler)    # Hook
agent.conversation.set_system_persistent_section("rules", "…")   # Persistent section of the system prompt
agent.state["counter"] = JsonEntry(0)        # State entry
agent.system("You are a ...")                # Replaces the system prompt body (persistent sections are not replaced)