Display & web service¶
The agent never calls print directly: it broadcasts events to a DisplayAbstract, and every confirmation request goes out through the same layer as well. This way the same execution logic can land in a terminal, in a browser, or be completely silent.
Call-side API (used internally by the agent)¶
| Method | Hook triggered | Event emitted |
|---|---|---|
display_event(payload) |
none | any event (no state restriction) |
info(message) |
before_display_info |
InfoEvent |
warning(message) |
before_display_warning |
WarningEvent |
error(message) |
before_display_error |
ErrorEvent |
get_choice(prompt, choices, ...) |
none | ConfirmEvent (source is "user" or "auto") |
get_confirm(prompt, ..., default=True) |
none | same as above (the options are Yes/No) |
Every method except display_event requires the agent to be in the Init state; the three display hooks may rewrite args.message in place, which is commonly used for filtering or adding a prefix (see the examples in the Extension system).
Event model¶
Events are pydantic models wrapped in one common envelope, DisplayEvent: timestamp, name (the class name), agent (AgentInfo: name / identifier / workdir) and payload.
| Event | Key fields |
|---|---|
UserMessageEvent |
content, images (kind is url or base64) |
UserCommandEvent |
name, arguments |
ModelWorkingEvent |
model_call_id, remaining_iterations |
ModelMessageEvent |
model_call_id, content, reasoning, total_tokens |
ToolCallEvent |
tool_call_id, tool_name, args |
ToolResultEvent |
tool_call_id, result |
ShowHistoryEvent / ShowHelpEvent / ShowToolsEvent |
history, command table, tool table |
InfoEvent / WarningEvent / ErrorEvent |
message |
ConfirmEvent |
choice, choices, source, prompt, message |
AgentBindEvent / AgentUnbindEvent |
none (bind/unbind signals) |
Custom display layer¶
You only need to implement two abstract methods:
from xun import Agent, DisplayAbstract, ToolBox
class RawDisplay(DisplayAbstract):
def on_event(self, event):
print(f"{event.name}: {event.payload}")
def get_choice(self, request) -> str:
print(f"[{request.prompt}] {request.choices}")
return input(">>> ")
with Agent(display=RawDisplay(), toolbox=ToolBox().with_defaults()) as agent:
agent.instruct("...").execute()
get_choice receives a DisplayAbstract.ChoiceRequest: agent_info, prompt, choices, message, title, subtitle, default, allow_extra; it returns the selected string.
Built-in implementations:
| Implementation | Behaviour |
|---|---|
NullDisplay() |
discards events; get_choice raises NotImplementedError. The default of a bare Agent() |
Display() |
console output (rich); the default of setup_agent and of xun |
WebDisplay(expose_files=False, max_events=2000) |
web backend, keeps the event history so the front end can replay it |
Web service programming interface¶
xuns is nothing but a wrapper around the following:
from xun import WebDisplay, WebDisplayService, setup_agent
display = WebDisplay(expose_files=True)
setup_agent(display=display, default_tools=True, workdir=".test")
service = WebDisplayService(host="localhost", port=18960, token="", base_path="")
service.mount("/", display)
service.start(blocking=False) # prints the access URL of every session
...
service.stop()
- With
token=""a random token is generated automatically; the three ways of passing--tokenand the routing conventions are described in xuns · Web service. port=0means a random port; the real port is written toservice.port.mount()can mount several display instances as long as their paths do not overlap; the/srvsegment is reserved.- The root path of the site redirects automatically to
/chat/; FastAPI's built-in/docsis disabled so that/docs/is left for the bundled documentation site.
Session factory¶
Once a session_manager is passed in, the interface can create and delete sessions on its own. The factory is a synchronous context manager that yields (mount_path, display), and its finally block performs the clean-up:
from contextlib import contextmanager
from uuid import uuid4
from xun import WebDisplay, WebDisplayService, setup_agent
@contextmanager
def session_manager():
display = WebDisplay(expose_files=True)
agent = setup_agent(display=display, default_tools=True, workdir=".test")
try:
yield f"/{uuid4()}", display
finally:
agent.finalize()
service = WebDisplayService(session_manager=session_manager)
web_session(manage_sessions=True) provides exactly this default factory (each session gets its own temporary working directory).
Front-end and back-end contract¶
| Endpoint | Description |
|---|---|
WS /ws |
main channel: the four message types message, command, choice, cancel |
GET /api/events |
fetches the missed events when reconnecting after a disconnect |
GET /api/prompts、POST /api/prompts/{id}/resolve |
confirmation requests awaiting an answer |
GET /api/agents、GET /api/running、GET /api/config |
agent list, running state, whether the file interface is exposed |
GET /api/commands/{agent_id}、GET /api/capabilities/{agent_id} |
command table, model and capabilities |
GET/POST /api/files/*、/api/serve/* |
file browsing and temporary static hosting (registered only when expose_files=True) |
WebSocket message bodies: message (agent_id, content, at most 8 images), command (name, arguments), choice (prompt_id, value), cancel (agent_id).
Documentation site
WebDisplayService mounts src/xun/assets/docs as a static site at {base}/docs/, with no token required; that site is exactly the artefact this directory produces with MkDocs and make doc-dist then copies in.