Skip to content

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 --token and the routing conventions are described in xuns · Web service.
  • port=0 means a random port; the real port is written to service.port.
  • mount() can mount several display instances as long as their paths do not overlap; the /srv segment is reserved.
  • The root path of the site redirects automatically to /chat/; FastAPI's built-in /docs is 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.