A2UI —— 声明式生成式 UI(可选插件)。
移植自 @ag-ui/a2ui-middleware(CopilotKit 在 agent-utils.ts 里与
Open Generative UI 挂在同一个位置)。两者的分工值得先说清楚:
copilotkit.genui(Open Generative UI,在 examples 里) | 本 ns(A2UI) | |
|---|---|---|
| 模型生成什么 | 一整块 HTML/CSS/JS | 组件树(只能用 catalog 里有的组件) |
| 谁来渲染 | 沙箱 iframe 执行模型写的代码 | 前端按自己的组件库渲染 |
| 可控性 | 任意 JS(所以要沙箱) | 白名单:模型编不出 catalog 之外的组件 |
| 交互回流 | Websandbox.connection.remote.* | forwardedProps.a2uiAction(见 input-transform) |
要「像素级自由」用 genui,要「设计系统一致 + 不执行任意代码」用 A2UI。
(rt/runtime {:agent-fn (tools/agent-fn (a2ui/with-tool spec))
:event-transform (a2ui/event-transform)})
注入工具与用法(with-tool):render_a2ui 工具 + 一段用法提示词 +
catalog(有哪些组件、各自什么属性)。catalog 是这条路的关键——它既是
给模型的词汇表,也是「模型编不出别的组件」的保证;
翻译事件(event-transform):模型调 render_a2ui,参数
{surfaceId, components, data} 翻成一条 ACTIVITY_SNAPSHOT:
{:activityType "a2ui-surface"
:messageId "a2ui-surface-<toolCallId>"
:replace true
:content {"a2ui_operations"
[{:version "v0.9" :createSurface {…}}
{:version "v0.9" :updateComponents {…}}
{:version "v0.9" :updateDataModel {…}}]}}
三条 op 的顺序是协议要求的(先建面,再给组件,最后灌数据);replace true
表示「整块换掉」——所以不需要 delta,每次都是完整的一份;
回流用户动作(input-transform):用户点了生成出来的按钮,前端把
forwardedProps.a2uiAction 发上来,翻成一句话喂回模型。
extractCompleteItems),一边生成一边往面上贴。我们的运行时不增量流式
工具参数(docs/agent-runtime-design.md §10 第 4 条),参数一次到齐,所以
一条快照发完——形状与顺序一致,少的是中间态;log_a2ui_event 的一次工具调用 + 工具结果塞进
历史。我们的历史是服务端权威(§7.3),客户端塞不进消息,所以翻成一句
用户消息(措辞与上游的 formatUserActionResult 一致)。A2UI —— **声明式**生成式 UI(可选插件)。
移植自 `@ag-ui/a2ui-middleware`(CopilotKit 在 `agent-utils.ts` 里与
Open Generative UI 挂在同一个位置)。两者的分工值得先说清楚:
| | `copilotkit.genui`(Open Generative UI,在 examples 里) | 本 ns(A2UI) |
|---|---|---|
| 模型生成什么 | 一整块 HTML/CSS/JS | **组件树**(只能用 catalog 里有的组件) |
| 谁来渲染 | 沙箱 iframe 执行模型写的代码 | 前端按自己的组件库渲染 |
| 可控性 | 任意 JS(所以要沙箱) | **白名单**:模型编不出 catalog 之外的组件 |
| 交互回流 | `Websandbox.connection.remote.*` | `forwardedProps.a2uiAction`(见 `input-transform`) |
要「像素级自由」用 genui,要「设计系统一致 + 不执行任意代码」用 A2UI。
## 装
```clojure
(rt/runtime {:agent-fn (tools/agent-fn (a2ui/with-tool spec))
:event-transform (a2ui/event-transform)})
```
## 后台干什么
1. **注入工具与用法**(`with-tool`):`render_a2ui` 工具 + 一段用法提示词 +
**catalog**(有哪些组件、各自什么属性)。catalog 是这条路的关键——它既是
给模型的词汇表,也是「模型编不出别的组件」的保证;
2. **翻译事件**(`event-transform`):模型调 `render_a2ui`,参数
`{surfaceId, components, data}` 翻成一条 `ACTIVITY_SNAPSHOT`:
{:activityType "a2ui-surface"
:messageId "a2ui-surface-<toolCallId>"
:replace true
:content {"a2ui_operations"
[{:version "v0.9" :createSurface {…}}
{:version "v0.9" :updateComponents {…}}
{:version "v0.9" :updateDataModel {…}}]}}
三条 op 的顺序是协议要求的(先建面,再给组件,最后灌数据);`replace true`
表示「整块换掉」——所以不需要 delta,每次都是完整的一份;
3. **回流用户动作**(`input-transform`):用户点了生成出来的按钮,前端把
`forwardedProps.a2uiAction` 发上来,翻成一句话喂回模型。
## 与上游的两处差异(都记在设计文档 §9.14)
1. 上游从**流式** tool-call 参数里增量抠出「已经完整的那几个组件」
(`extractCompleteItems`),一边生成一边往面上贴。我们的运行时不增量流式
工具参数(docs/agent-runtime-design.md §10 第 4 条),参数一次到齐,所以
**一条快照发完**——形状与顺序一致,少的是中间态;
2. 上游把用户动作合成成 `log_a2ui_event` 的一次**工具调用 + 工具结果**塞进
历史。我们的历史是服务端权威(§7.3),客户端塞不进消息,所以翻成一句
**用户消息**(措辞与上游的 `formatUserActionResult` 一致)。activity 消息的类型标签。前端按它挑 renderer。
activity 消息的类型标签。前端按它挑 renderer。
(basic-catalog)A2UI v0.9 基础 catalog(18 个组件)。见文件末尾 basic-components。
A2UI v0.9 基础 catalog(18 个组件)。见文件末尾 `basic-components`。
A2UI v0.9 基础 catalog 的 id。CopilotKit 的 a2ui-renderer 默认实现的就是它。
A2UI v0.9 基础 catalog 的 id。CopilotKit 的 `a2ui-renderer` 默认实现的就是它。
生成的,别手改:由 @a2ui/web_core 的
src/v0_9/schemas/catalogs/basic/catalog.json 摘出来(组件名 + 属性 +
属性描述 + 枚举 + 必填),去掉了 $ref 与公共部分——那些是给校验器看的,
模型只需要词汇表。要换成你自己的组件库,给 with-tool 传 :catalog。
**生成的**,别手改:由 `@a2ui/web_core` 的 `src/v0_9/schemas/catalogs/basic/catalog.json` 摘出来(组件名 + 属性 + 属性描述 + 枚举 + 必填),去掉了 `$ref` 与公共部分——那些是给校验器看的, 模型只需要词汇表。要换成你自己的组件库,给 `with-tool` 传 `:catalog`。
工具描述——逐句移植自上游的 RENDER_A2UI_TOOL。
工具描述——逐句移植自上游的 `RENDER_A2UI_TOOL`。
(event-transform)(event-transform {:keys [tool-name catalog-id]
:or {tool-name tool-name catalog-id basic-catalog-id}})runtime 的 :event-transform:render_a2ui 的调用 → surface 快照。
与 genui 的一处不同:不扣住 :tool/started。上游 A2UI 也不扣——
工具卡片先出、面随后贴上,是它接受的顺序(genui 那边扣住是因为沙箱 UI
一闪的观感问题,两边取舍不同,照各自的来)。
`runtime` 的 `:event-transform`:`render_a2ui` 的调用 → surface 快照。 与 `genui` 的一处不同:**不扣住 `:tool/started`**。上游 A2UI 也不扣—— 工具卡片先出、面随后贴上,是它接受的顺序(genui 那边扣住是因为沙箱 UI 一闪的观感问题,两边取舍不同,照各自的来)。
(guidelines tool)用法提示词——移植自上游的 RENDER_A2UI_TOOL_GUIDELINES。
这是提示词不是文档:flat 组件数组、root 必须是布局组件、数据绑定语法、 重复渲染语法、按钮 action 的形状,模型全靠这段学会。
用法提示词——移植自上游的 `RENDER_A2UI_TOOL_GUIDELINES`。 **这是提示词不是文档**:flat 组件数组、root 必须是布局组件、数据绑定语法、 重复渲染语法、按钮 action 的形状,模型全靠这段学会。
(input-transform)routes/start! 的 :input-transform:把 A2UI 的用户动作接进本轮输入。
前端点按钮时发的那一轮可能没有用户消息(就是个动作),所以只在
:message 空的时候顶上;两者都有就把动作附在后面(用户既打了字又点了按钮)。
`routes/start!` 的 `:input-transform`:把 A2UI 的用户动作接进本轮输入。 前端点按钮时发的那一轮**可能没有用户消息**(就是个动作),所以只在 `:message` 空的时候顶上;两者都有就把动作附在后面(用户既打了字又点了按钮)。
content 里装 op 数组的键。字符串键——它要原样出现在 JSON 里。
`content` 里装 op 数组的键。**字符串键**——它要原样出现在 JSON 里。
JSON Schema,对齐上游 RENDER_A2UI_TOOL.parameters。
没有 catalogId 参数:catalog 由宿主定,不能让模型自己编一个前端没注册的。
JSON Schema,对齐上游 `RENDER_A2UI_TOOL.parameters`。 **没有 catalogId 参数**:catalog 由宿主定,不能让模型自己编一个前端没注册的。
(render-tool)(render-tool name)内联工具 map。
handler 不画任何东西——UI 就是参数本身,后台只负责翻译成 activity 事件;
handler 只给模型一个「贴上去了」的回执好让它继续说话(与 genui 同款取舍)。
内联工具 map。 **handler 不画任何东西**——UI 就是参数本身,后台只负责翻译成 activity 事件; handler 只给模型一个「贴上去了」的回执好让它继续说话(与 `genui` 同款取舍)。
catalog 注入模型上下文时的抬头(上游的 A2UI_SCHEMA_CONTEXT_DESCRIPTION 同位)。
catalog 注入模型上下文时的抬头(上游的 `A2UI_SCHEMA_CONTEXT_DESCRIPTION` 同位)。
(surface-event tool-call-id args catalog-id)一条 :activity/snapshot。:replace true = 整块换掉,所以不需要 delta。
一条 `:activity/snapshot`。`:replace true` = 整块换掉,所以不需要 delta。
(surface-ops {:keys [surfaceId components data]} catalog-id)render_a2ui 的参数 → A2UI v0.9 op 数组。
顺序是协议要求的:先 createSurface 建面,再 updateComponents 给组件,
最后(有 data 才发)updateDataModel 灌数据。少一条或反了,前端要么空白要么
报「surface 不存在」。
`render_a2ui` 的参数 → A2UI v0.9 op 数组。 **顺序是协议要求的**:先 `createSurface` 建面,再 `updateComponents` 给组件, 最后(有 data 才发)`updateDataModel` 灌数据。少一条或反了,前端要么空白要么 报「surface 不存在」。
(user-action-message {:keys [name surfaceId sourceComponentId context]})forwardedProps.a2uiAction.userAction → 一句喂回模型的话。
措辞与上游 formatUserActionResult 一致(它把这句话作为 log_a2ui_event
的工具结果塞进历史;我们的历史是服务端权威,客户端塞不进消息,所以作为
用户消息进来——语义相同,路径不同)。
`forwardedProps.a2uiAction.userAction` → 一句喂回模型的话。 措辞与上游 `formatUserActionResult` 一致(它把这句话作为 `log_a2ui_event` 的工具结果塞进历史;我们的历史是服务端权威,客户端塞不进消息,所以作为 **用户消息**进来——语义相同,路径不同)。
(with-tool spec)(with-tool spec
{:keys [catalog tool-name]
:or {catalog (basic-catalog) tool-name tool-name}})把 render_a2ui 工具、用法提示词、catalog 挂进一份 create-agent 配置。
:catalog {:catalogId … :components {名字 → JSON Schema}},缺省用
basic-catalog。传你前端真正注册的那份——catalog 是模型的
词汇表,报了前端没有的组件,渲染就是空白;:tool-name 自定义工具名(上游允许,默认 render_a2ui)。把 `render_a2ui` 工具、用法提示词、catalog 挂进一份 `create-agent` 配置。
- `:catalog` `{:catalogId … :components {名字 → JSON Schema}}`,缺省用
`basic-catalog`。**传你前端真正注册的那份**——catalog 是模型的
词汇表,报了前端没有的组件,渲染就是空白;
- `:tool-name` 自定义工具名(上游允许,默认 `render_a2ui`)。cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |