A mini LLM agent framework with function-based tools and sub-agent spawning.
The core codebase is compact: about 3000 lines in src/xun/*.py, with comprehensive type hints.
Requires Python 3.12+ (PEP 695)
# 1. Install dependencies
pip install git+https://github.com/MenxLi/xun.git
# 2. Install Playwright browsers (if using the default browser tools)
playwright install
# 3. Configure environment variables (see `Configuration` section below)
vim .env
# 4. Run the agent in interactive mode
xunOptionally, run the agent in web mode:
# - Build the web frontend (if using the web display)
make build-web
# - Start the web server at current directory
xuns
# - Use a temporary directory as the workspace instead
xuns ""Each positional argument of xuns creates an agent rooted at that directory (default: current directory; an empty string uses a temporary directory that is removed on exit).
Basic: Quickly set up an agent with plain functions as tools — no decorators, no classes needed.
from xun import setup_agent
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
agent = setup_agent(tools = [add])
agent.instruct("Add 2 and 3.").execute()Advanced: The framework is flexible and extensible. Additional features are shown in demo.ipynb, including:
Agentconfiguration- Display extension
- Output validation
- Tool attributes
- Context injection
- Type-state transition
- Sub-agent spawning
- Lifecycle hooks
- ...
Do check out demo.ipynb for detailed examples.
Run xun in your terminal to start an interactive session.
You can also pass a prompt as an argument to begin with a specific instruction.
xun "Write a hello world python script and save it to hello.py"Image attachments are supported in the format of [image:path_or_url]. For example:
>>> [image:cat.png image:https://example.com/dog.png] compare them.
Input /help to see the full list of commands.
WebDisplay provides an interactive web interface for the agent.
It can be used as a chat-based web application, or as a backend for other applications.
from xun import WebDisplay, WebDisplayService, setup_agent
display = WebDisplay(expose_files=True)
agent = setup_agent(display=display, default_tools=True)
service = WebDisplayService().mount("/", display)
service.start(blocking=True)Open any tokenized URL printed at startup. The service exchanges its query token for one HttpOnly cookie scoped to /, so the browser can access every mounted display without logging in again. API clients can use Authorization: Bearer <token>.
File browsing, upload, download, and deletion are disabled unless expose_files=True.
The agent will start in web mode, and you can access it via the printed URL.
Multiple displays can share one authenticated service. Each mount keeps its own agents, event history, and file policy:
service = WebDisplayService()
service.mount("/research", research_display)
service.mount("/coding", coding_display)
service.start(blocking=True)display.build_routes() and display.build_app() do not add authentication. Use WebDisplayService for the authenticated server, or provide authentication and lifecycle handling in your own ASGI host.
Build the image and run the agent in a container with xunc:
make build-docker # builds the web frontend, then the `xun` image
xunc # sandbox: no mount, container starts from the image's own /workspace
xunc . # mount the current directory as /workspace inside the containerBy default xunc starts xuns --host 0.0.0.0 in the container and publishes port 18960 (bridge network), so the web UI is reachable from the host at the tokenized URL printed at startup. Options:
--exec CMD: command to run inside the container (e.g.--exec bashfor a plain shell,--exec ""for the image's default CMD)--port LIST: ports to publish in bridge mode (default18960)--network host: host networking (on macOS this is the Docker VM's network namespace, which is not reachable from a host browser — prefer the default bridge mode there)--env PATTERNS: environment variables to forward, comma-separated wildcards (defaultXUN_*)--image/--name: image (defaultxun) and container name
Frontend development
The frontend development command starts both the backend and Vite with Vue DevTools:cd web
npm install
npm run devOpen http://127.0.0.1:5173. Build a production bundle with npm run build. See web/README.md for connecting the UI to a separately managed backend.
xun reads its configuration from ~/.xun/config.json (the location can be overridden with the XUN_HOME environment variable). The file is optional:
if it does not exist, built-in defaults are used, and no files are created.
The file only needs to contain the fields you want to change. For example, to just override the model:
{
"model": {
"name": "my-model"
}
}The config supports ${XUN_...} placeholders which are substituted from environment variables (e.g. ${XUN_OPENAI_API_KEY}), so secrets can live in a .env file instead. A placeholder with no matching environment variable causes a startup error.
| Config field | Built-in default | Description |
|---|---|---|
provider.openai_base_url |
${XUN_OPENAI_BASE_URL} |
OpenAI-compatible API endpoint. |
provider.openai_api_key |
${XUN_OPENAI_API_KEY} |
API key. |
model.name |
${XUN_OPENAI_MODEL} (empty) |
Model identifier. If the resolved value is empty, available models are auto-detected from the API. |
model.capabilities |
["vision"] |
Capabilities exposed to the model (e.g. vision for image input). |
auto_confirm |
false |
Auto-approve actions without prompting. |