Skip to content

Tool system

Functions as tools

Hand an ordinary function to ToolBox and the framework derives its name, description and parameter JSON Schema automatically:

from xun import ToolBox

def list_dir(path: str, details: bool = False) -> dict:
    """List the contents of a directory.
    Returns names of files and directories; with details also sizes."""
    ...

toolbox = ToolBox().register(list_dir)

Derivation rules:

Item Rule
Tool name @tool_attr(name=...) takes priority, otherwise the function name is used
Description the whole docstring verbatim (no Google/reST parsing), so the meaning of parameters has to be written clearly in the docstring
Parameter model pydantic.create_model(), extra="forbid": passing extra fields to the model fails validation outright
Parameter types taken from the type annotations; with no annotation but a non-None default value, type(default) is used; otherwise Any
Required detection no default value means required; Optional[int] = None is nullable and not required; Literal[...] becomes an enum
Validation model_validate(..., strict=True) on invocation — no implicit conversion, passing "3" to an int is rejected. The exception is list / tuple parameters: when the model writes an array as a string ('["a", "b"]' or "a, b") it is parsed automatically, Optional[list[...]] included, while unions that contain a plain str member are left untouched; elements of the comma-separated form stay strings, so use the JSON array form when the elements are numbers

self/cls and ToolCallContext parameters are automatically excluded from the schema.

ToolBox API

Method Description
register(*funcs) register several functions (except_safe is applied automatically); on a name clash override decides the winner, otherwise ValueError: Conflict tool name
tool(func) decorator form of register
with_defaults(*groups) register built-in tool groups; calling it with no arguments registers all 8 groups
with_subagent_provider(agent_getter=None) register agent_run / agent_run_parallel; agent_getter takes an AgentGetterParam and returns an uninitialised sub-agent
disable(*names) / enable(*names) supports fnmatch wildcards (browser_*); disabling is not deleting, list_tools() filters the tools out
disable_subagent() / enable_subagent() turns off only the two sub-agent tools
list_tools(model_capabilities=None) filters out disabled tools and tools whose required model capabilities are not met
list_tools_json(model_capabilities=None) the tools array handed to the model
call_tool(agent, tool_name, arguments, context) the unified call entry point
clone() copies the tool table and the set of disabled names; the Function objects themselves are still shared with the source toolbox (not a deep copy)

Built-in tool group names: system, framework, fs, patch, cmd, search, browser, diagnostic. For the item-by-item list see Built-in tool reference.

tool_attr

from xun import tool_attr

@tool_attr(name="MultiplyTool")
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
Attribute Type Default Purpose
name str \| None None overrides the tool name exposed to the model (used heavily by built-in tools, e.g. system_time → datetime)
required_capabilities Sequence[ModelCapabilityType] [] currently only "vision"; when the model's capabilities do not include it, the tool does not appear in the request
override bool False allows silently replacing a tool with the same name (used when an extension overrides a built-in tool)

tool_attr only attaches metadata, it does not register anything; registration still goes through ToolBox.register(). When debugging name clashes, set _XUN_INFO_TOOL_OVERRIDE=1 to print the override information.

Context injection

A tool that needs the agent itself or the data passed in by the caller only has to declare a ToolCallContext parameter:

from xun import ToolCallContext as Context
import datetime

def stamp(context: Context[dict]) -> str:
    """Record the call time into the shared context."""
    context.value["tool_call_time"] = datetime.datetime.now().isoformat()
    return context.value["tool_call_time"]

agent.instruct("Call your only tool.").execute(context={"tool_call_time": "?"})
Accessor Contents
ctx.agent the agent making the call (Agent[Agent.T.Init])
ctx.tool_name the registered name (may differ from the Python function name)
ctx.value the arbitrary value passed by Agent.execute(context=...), passed through every layer

ToolCallContext[T] is generic, and T is the type of value; the fields are private and accessed through read-only properties.

Return values and exceptions

ToolBox.register() wraps every tool in except_safe:

  • returning a plain value → automatically wrapped as Result.Ok(value)
  • raising an exception → Result.Err(ErrorInfo(error=str(exc), details=repr(exc)))
  • KeyboardInterrupt / CancelledError propagate unchanged

So inside a tool it is enough to return or raise; you do not need to build a Result by hand. Serialisation goes through model_dump / to_json / value_json; when serialisation fails nothing propagates: the tool result becomes the text [Error] Failed to serialize tool result: ....

Images

Images cannot enter the context directly as a tool return value. defer_tool_image(ctx, image, msg="") registers a one-shot after_execution_step hook that appends a user message carrying the image (and optionally one line of explanation) after this round's tool results (request_image and browser_screenshot work exactly this way). Constraints: PNG/JPEG/WebP/GIF, at most 10 MB per image; request_image additionally caps the long side at 1000 px after cropping and reports the final and original sizes in that line.

Paths

Every path parameter is validated by Workspace.resolve(): relative paths are resolved against workdir, and only workdir and the lazy temporary directory are allowed (the temporary directory only counts as workspace once it has actually been created); anything outside reports Path ... is not within the agent's workspace. Paths in return values are usually given as absolute paths (exceptions: glob and grep return paths relative to the search root, list_dir returns bare entry names, and the apply_patch summary is relative to the patch directory).

Permission and confirmation policy

Calls that need human approval all go through agent.get_choice() / agent.get_confirm(), which return ChoiceOutcome[T] (choice plus source, which is "user" or "auto").

ChoiceOutcome has no truth value

Writing if agent.get_confirm(...) directly raises TypeError. Use .choice: if agent.get_confirm("Proceed?").choice: ...

Scenario Behaviour
writing an existing file, copy overwrite, delete, move the check looks at the destination, except for move, which looks at the source (moving equals deleting the source); writes inside the lazy temporary directory go through without asking; anything else pops the three-option "write permission" prompt: allow and grant / allow this time only / deny; only choosing "grant" writes to the write allowlist; apply_patch and mkdir only run workspace path validation and do not go through this confirmation
bash commands the command allowlist is checked first; when the command is not in the list or contains dangerous operators, a "command risk assessment" sub-agent (read-only tools only) decides allow/unsure/reject, which then determines whether a confirmation prompt is shown
config.auto_confirm is true returns the default option directly and marks source="auto"; it does not write to any allowlist
ask_preference tool explicitly skips automatic confirmation, always asks

The built-in command allowlist covers common diagnostic and text-processing commands (ls, cat, git status, git diff, python -m pytest and so on — note that curl, wget, sed and journalctl in that list are not read-only) and matches by token prefix, so git diff --cached matches git diff while git statusx does not. bash splits the command into segments and checks: whether the executable is in the allowlist, whether an executable at an unusual path is used, backquote substitution, newline concatenation, $(...) sub-substitution and the like; among the shell operators only ;, &&, ||, |, (, ) are automatically allowed, and the rest (& background, >, >>, <, <<, >&, <&) all need confirmation; for redirections only the safe forms > /dev/null, 1>/dev/null, 2>/dev/null and 2>&1 are exempt from confirmation.

The allowlists are stored per agent instance in state["__builtin_tool_policy"], and the /policy family of commands inspects and adds or removes entries, see xun · Terminal session.

Windows

The cmd group does not register the bash tool when os.name == "nt", and prints a skip notice once during the registration phase.