跳转至

显示扩展

智能体不直接打印,它只发事件。显示层负责渲染事件、并把人机交互(确认、选择)带回给智能体。 因此同一个智能体可以安静地跑在脚本里,也可以推到控制台或浏览器。

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 都可以就地修改,等于给输出加了一道过滤器。