显示扩展¶
智能体不直接打印,它只发事件。显示层负责渲染事件、并把人机交互(确认、选择)带回给智能体。 因此同一个智能体可以安静地跑在脚本里,也可以推到控制台或浏览器。
flowchart LR
A["Agent"] -->|"info / display_event / get_choice"| D["DisplayAbstract"]
D --> B1["NullDisplay<br/>静默"]
D --> B2["Display<br/>控制台"]
D --> B3["WebDisplay<br/>浏览器"]
D --> B4["自定义显示"]
B2 -.确认结果.-> A
B3 -.确认结果.-> A
内置显示¶
| 显示 | 用途 | 默认缓冲 |
|---|---|---|
NullDisplay() |
什么都不做,Agent() 的默认值,适合程序化调用 |
0 |
Display(event_buffer_size=1000) |
控制台渲染 + prompt_toolkit 输入,setup_agent 的默认值 |
1000 |
WebDisplay(expose_files=False, event_buffer_size=5000) |
推给浏览器,见 WebDisplay 挂载 | 5000 |
与显示层的关系¶
绑定的时机由框架管:initialize() 里 display.bind(agent) 并发 AgentBindEvent,
finalize() 里 unbind 后再发 AgentUnbindEvent。显示层可以服务多个智能体
(display.agents 按 identifier 区分),但一个智能体只有一个显示层。
from xun import Agent, WebDisplay
d = WebDisplay(expose_files=True)
setup_agent(display=d, workdir=".test") # 两个智能体共用一个显示层
setup_agent(display=d, workdir=".tmp")
Agent.inherit(parent, share_display=False) 会给子智能体一个新的 NullDisplay,
让它不往用户的界面上刷输出。
事件模型¶
每个事件都是一个信封:
class DisplayEvent(BaseModel, Generic[DisplayEventT]):
timestamp: float # 事件时间
name: str # 负载类名,例如 "ModelMessageEvent"
agent: AgentInfo # name / identifier / workdir
payload: DisplayEventT
负载类型是一个闭合联合,共 19 种:
| 事件 | 关键字段 | 触发点 |
|---|---|---|
AgentBindEvent / AgentUnbindEvent |
— | 绑定 / 解绑 |
AgentRunningStartEvent / AgentRunningEndEvent |
— | 最外层 run_scope() 进出 |
UserMessageEvent |
content, images[] |
instruct() |
UserCommandEvent |
name, arguments |
execute_command() |
ModelWorkingEvent |
model_call_id, remaining_iterations |
每轮模型调用前 |
ModelMessageEvent |
model_call_id, content, reasoning, total_tokens |
模型给出内容时 |
ToolCallEvent |
tool_call_id, tool_name, args |
工具调用(参数已做 JSON 修复) |
ToolResultEvent |
tool_call_id, result |
工具成功返回时 |
ConfirmEvent |
choice, choices, source, prompt, message |
确认问答(source 为 user 或 auto) |
InfoEvent / WarningEvent / ErrorEvent |
message |
agent.info() 等 |
HTMLInfoEvent |
html, title |
Web 端按富文本渲染的信息 |
ShowHelpEvent / ShowToolsEvent / ShowHistoryEvent / ShowExtensionsEvent |
各自的列表 | /help /tools /history /extensions |
不要自造事件类
DisplayEvent.payload 在运行时按这个联合校验,自定义的 BaseModel 会被拒。
要输出自定义内容,复用 HTMLInfoEvent(Web 端富文本)或 InfoEvent;
真要新增事件类型,得改 src/xun/display_event.py 并加入联合。
文本流式增量不是事件,走 model_text_delta 钩子,见 Hooks。
事件缓冲与回放¶
显示层自带环形缓冲,默认容量为 0——负载可能很大(工具结果、base64 图片), 所以由消费方自己决定留多少:
d = MyDisplay().with_buffer_size(2000) # 链式调用,返回 self
d.events() # 全部事件(可按 agent_id 过滤)
d.clear_events(agent.identifier)
html = d.render_history_as_html(title="会话回顾") # 渲染成独立 HTML
/render <文件> 用的就是 render_history_as_html。
写一个自定义显示¶
只需要实现两个抽象方法:
from xun import Agent, DisplayAbstract
class RawDisplay(DisplayAbstract):
def on_event(self, event):
print(f"{event.name}: {event.payload}")
def get_choice(self, request) -> str:
raise NotImplementedError("这个示例里不需要人机交互")
with Agent(display=RawDisplay()) as agent:
agent.toolbox.register(weather)
result = agent.instruct("What is the weather in Paris?").execute()
on_event(event):收到事件时调用,同步、在发出事件的线程上执行,别在这里做长耗时操作。get_choice(request):智能体请求用户选择时调用,返回choices中的一个字符串。
不要直接调用显示层方法
正确姿势永远是走智能体:agent.info(...)、agent.display_event(...)、agent.get_choice(...)。
直接调 display.on_event 会跳过缓冲与 before_display_* 钩子。
智能体侧的显示接口¶
| 方法 | 说明 |
|---|---|
info / warning / error(message) |
先过对应的 before_display_* 钩子(消息可就地改),再发事件 |
display_event(payload) |
发任意事件负载 |
get_choice(prompt, choices, ..., default=, allow_extra=, skip_auto_confirm=) |
返回 ChoiceOutcome[str] |
get_confirm(prompt, ..., default=True) |
返回 ChoiceOutcome[bool] |
ChoiceOutcome 带 choice 与 source("user" 或 "auto"),没有布尔值:
if outcome: 会直接抛 TypeError,必须写 if outcome.choice:。
自动确认(auto_confirm)时返回 default,没有 default 则取 choices[0] 并额外发一条告警;
skip_auto_confirm=True 可以强制问人(ask_preference 工具就是这么做的)。
只想旁观输出而不实现整个显示层时,用钩子更省事:
agent.hooks.before_display_error.add(
lambda args: log.write(args.message)
)
三个钩子的 args.message 都可以就地修改,等于给输出加了一道过滤器。