Liking cljdoc? Tell your friends :D

im.ttalk.agent.agui.codec

中立事件 ⇄ AG-UI 协议。

runtime 发中立事件,协议知识全在这一层——换个前端协议只换这个 ns, runtime / event / emit 一个字都不用改(对称于「core 收回厂商 wire 知识」, 见 docs/response-path-consolidation.md)。

本 ns 不产出 HTTP 任何东西:返回的是 Clojure map,SSE 帧、路由、CORS 在 examples/copilotkit/(design-principles §2)。

AG-UI 字段名逐个对过 CopilotKit 的 middleware-sse-parser.test.ts: TOOL_CALL_START 是 toolCallName 不是 name,TOOL_CALL_ARGS 的 delta 是 JSON 字符串不是对象——这类地方猜错了前端不会报错,只会静默 少渲染一块。

中立事件 ⇄ AG-UI 协议。

**runtime 发中立事件,协议知识全在这一层**——换个前端协议只换这个 ns,
runtime / event / emit 一个字都不用改(对称于「core 收回厂商 wire 知识」,
见 docs/response-path-consolidation.md)。

本 ns **不产出 HTTP 任何东西**:返回的是 Clojure map,SSE 帧、路由、CORS
在 examples/copilotkit/(design-principles §2)。

AG-UI 字段名逐个对过 CopilotKit 的 `middleware-sse-parser.test.ts`:
`TOOL_CALL_START` 是 `toolCallName` 不是 `name`,`TOOL_CALL_ARGS` 的
`delta` 是 **JSON 字符串**不是对象——这类地方猜错了前端不会报错,只会静默
少渲染一块。
raw docstring

->aguiclj

(->agui ev)

中立事件 → AG-UI 事件 map(nil = 没有对应物),带 lane 归属。

中立事件 → AG-UI 事件 map(nil = 没有对应物),带 lane 归属。
sourceraw docstring

->agui-eventsclj

(->agui-events {:keys [type message-id] :as ev})

中立事件 → 零或一条 AG-UI 事件(没有对应物则空)。

曾经是一对多::run/paused 发一条 CUSTOM 说「停在这儿等人」,再补一条 RUN_FINISHED 把 run 收口——AG-UI 的 run 必须有终态,少了它 stock 前端不 报错,只是 HTTP 请求永远不结束、工具卡片永远 inProgress(联调实测)。 改用 interrupt 协议后两件事合成一条:RUN_FINISHED 自带 outcome,既是 终态又说清了停因。续跑仍是新的 run,前端 connect 接着看(§4.7)。

留着这个函数(而不是让传输层直接调 ->agui)是对的——思考块马上又用上了: 一条 :reasoning/started 要发 REASONING_START + REASONING_MESSAGE_START 两条(外层是「这一段在想」,内层才是那条 reasoning 消息),收口对称。 ->agui 给的是内层那条,只认单条的调用方(如 live 脚本的假连接)因此也 看得见思考内容,只是少了外层括号。

中立事件 → **零或一条** AG-UI 事件(没有对应物则空)。

曾经是一对多:`:run/paused` 发一条 `CUSTOM` 说「停在这儿等人」,再补一条
`RUN_FINISHED` 把 run 收口——AG-UI 的 run 必须有终态,少了它 stock 前端不
报错,只是 HTTP 请求永远不结束、工具卡片永远 `inProgress`(联调实测)。
改用 interrupt 协议后两件事合成一条:`RUN_FINISHED` 自带 `outcome`,既是
终态又说清了停因。续跑仍是**新的 run**,前端 `connect` 接着看(§4.7)。

留着这个函数(而不是让传输层直接调 `->agui`)是对的——思考块马上又用上了:
一条 `:reasoning/started` 要发 `REASONING_START` + `REASONING_MESSAGE_START`
两条(外层是「这一段在想」,内层才是那条 reasoning 消息),收口对称。
`->agui` 给的是内层那条,只认单条的调用方(如 live 脚本的假连接)因此也
看得见思考内容,只是少了外层括号。
sourceraw docstring

agui->messagesclj

(agui->messages messages)

AG-UI 消息数组 → 中立消息。

入站方向,message->agui 的反面。主路径用不着它——/run 的历史取服务端 权威(见 parse-run-input)。用得着的是无状态 run(/suggest):那条路 没有服务端线程,客户端发上来的就是全部上下文。

认不出角色的整条丢掉(客户端 SDK 各有各的扩展,宁可少喂也不喂脏数据)。

AG-UI 消息数组 → 中立消息。

**入站方向,`message->agui` 的反面**。主路径用不着它——`/run` 的历史取服务端
权威(见 `parse-run-input`)。用得着的是**无状态 run**(`/suggest`):那条路
没有服务端线程,客户端发上来的就是全部上下文。

认不出角色的整条丢掉(客户端 SDK 各有各的扩展,宁可少喂也不喂脏数据)。
sourceraw docstring

agui-content->neutralclj

(agui-content->neutral content*)

AG-UI 的 InputContent 数组 → 中立多模态部件(im.ttalk.agent.model.content)。

字符串原样返回。 纯文本消息的形状一个字都不能变 —— 客户端也是这么做的 (没附件就不升成数组),两边一致,"你好" 这种消息才不会平白走部件路径。

映射(协议侧 → 中立侧):

AG-UI中立
{type:"text", text}{:type :text :text …}
{type:"image"/"audio"/"video"/"document", source:{type:"data", value, mimeType}}{:type :file :media-type … :data …}
同上但 source.type = "url"{:type :file :media-type … :url …}

image / audio / video / document 在中立层不各立一类,统一是 :file + media-type(与 Vercel AI SDK 的部件模型一致)—— wire 层按 media type 的顶层 类别分派,新格式不用加新类型。

⚠ 不做这一跳的话,部件数组会被 (str content) 压成一坨字符串喂给模型: 不报错,只是内容全丢,比报错难查得多。

AG-UI 的 `InputContent` 数组 → 中立多模态部件(`im.ttalk.agent.model.content`)。

**字符串原样返回。** 纯文本消息的形状一个字都不能变 —— 客户端也是这么做的
(没附件就不升成数组),两边一致,`"你好"` 这种消息才不会平白走部件路径。

映射(协议侧 → 中立侧):

| AG-UI | 中立 |
| --- | --- |
| `{type:"text", text}` | `{:type :text :text …}` |
| `{type:"image"/"audio"/"video"/"document", source:{type:"data", value, mimeType}}` | `{:type :file :media-type … :data …}` |
| 同上但 `source.type = "url"` | `{:type :file :media-type … :url …}` |

image / audio / video / document 在中立层**不各立一类**,统一是 `:file` +
media-type(与 Vercel AI SDK 的部件模型一致)—— wire 层按 media type 的顶层
类别分派,新格式不用加新类型。

⚠ 不做这一跳的话,部件数组会被 `(str content)` 压成一坨字符串喂给模型:
**不报错,只是内容全丢**,比报错难查得多。
sourceraw docstring

context->promptclj

(context->prompt context)

AG-UI RunAgentInput.context → 一段 system 文本。返回 nil = 这轮没有上下文。

条目是 {description, value}(ContextSchema,两个都是字符串)。前端 useAgentContext({description, value}) 注册,客户端每次 run 都重发全量 ——所以它天然是 turn 级的:这一轮带就有,下一轮不带就没了。落到我们这边的 出口是 chat-async 的 :extra-system-prompts(追加,不覆盖 agent 人设), 同样不进 ChatMemory。

value 按 schema 是字符串,但 useAgentContext 收的是 JsonSerializable, 路上谁 stringify 由客户端版本决定——所以这里两种都认,非字符串按 JSON 打平。

开头那句抬头是给模型的出处说明:不写的话这几行会被当成用户指令,而它们 其实是页面状态。

AG-UI `RunAgentInput.context` → 一段 system 文本。返回 nil = 这轮没有上下文。

条目是 `{description, value}`(`ContextSchema`,两个都是字符串)。前端
`useAgentContext({description, value})` 注册,客户端**每次 run 都重发全量**
——所以它天然是 turn 级的:这一轮带就有,下一轮不带就没了。落到我们这边的
出口是 `chat-async` 的 `:extra-system-prompts`(**追加**,不覆盖 agent 人设),
同样不进 ChatMemory。

`value` 按 schema 是字符串,但 `useAgentContext` 收的是 `JsonSerializable`,
路上谁 stringify 由客户端版本决定——所以这里两种都认,非字符串按 JSON 打平。

开头那句抬头是给模型的**出处说明**:不写的话这几行会被当成用户指令,而它们
其实是页面状态。
sourceraw docstring

events->aguiclj

(events->agui events)

批量转换并丢掉没有对应物的(保持顺序)。

批量转换并丢掉没有对应物的(保持顺序)。
sourceraw docstring

interrupt-idclj

(interrupt-id {:keys [run-id pending-tool]})

一次暂停的稳定标识 —— 发出去是 RUN_FINISHED.outcome.interrupts[].id, 客户端原样送回在 RunAgentInput.resume[].interruptId 里。

取 pending 工具的 tool-call id:它本来就唯一,且与同一条 interrupt 的 toolCallId 同源,前端把审批条挂回那张工具卡片不用二次关联。没有 pending 工具的暂停(纯 ask-user)退回 run-id。

入参既是暂停事件也是 runtime/awaiting 的返回值——两者 :run-id / :pending-tool 同形,路由侧据此校验「客户端回的是不是当前这一条」。

一次暂停的稳定标识 —— 发出去是 `RUN_FINISHED.outcome.interrupts[].id`,
客户端原样送回在 `RunAgentInput.resume[].interruptId` 里。

取 pending 工具的 **tool-call id**:它本来就唯一,且与同一条 interrupt 的
`toolCallId` 同源,前端把审批条挂回那张工具卡片不用二次关联。没有 pending
工具的暂停(纯 ask-user)退回 run-id。

**入参既是暂停事件也是 `runtime/awaiting` 的返回值**——两者 `:run-id` /
`:pending-tool` 同形,路由侧据此校验「客户端回的是不是当前这一条」。
sourceraw docstring

interrupts-ofclj

(interrupts-of {:keys [pending-tools] :as ev})

暂停事件 → outcome.interrupts:同一批里每条被叫停的调用一个 interrupt (designs/agui-borrow.md §2,对照 Keelson ChatAguiRunState.Judge / Done)。

只有一条时与从前逐字相同(interrupt-of 整个事件)——单审批的客户端不受影响。 多条时每条用自己的 tool-call id 作 interrupt id、自己的 pendingTool、自己的 前端 / 审批归类;reason 取 gate 给的审批理由(:approval :reason),没有就是 「需要审批: <工具名>」——整批合在一起的那句「需要审批: a, b」挂不到单张卡片上。

暂停事件 → `outcome.interrupts`:同一批里**每条**被叫停的调用一个 interrupt
(designs/agui-borrow.md §2,对照 Keelson `ChatAguiRunState.Judge` / `Done`)。

只有一条时与从前逐字相同(`interrupt-of` 整个事件)——单审批的客户端不受影响。
多条时每条用自己的 tool-call id 作 interrupt id、自己的 `pendingTool`、自己的
前端 / 审批归类;`reason` 取 gate 给的审批理由(`:approval :reason`),没有就是
「需要审批: <工具名>」——整批合在一起的那句「需要审批: a, b」挂不到单张卡片上。
sourceraw docstring

message->aguiclj

(message->agui m)
(message->agui m idx)

中立消息 → AG-UI 消息。

:id 优先用消息自己的——ChatMemory 落库时补的那个(msg/ensure-id), 跨快照稳定。没有才按位置合成(m-<idx>):那只保证「同一次快照内部自洽」, replace-tool-results / heal-dangling 一让位置漂,同一条消息在两次快照里 就是两个 id。走内置 store 的都有 id,退路是留给第三方 store 的。

与事件流的 message-id 不是一个 id 空间(那边是 <run-id>-mN)——合成一个 要让发射器的 id 流进 response->neutral,那是 core 依赖 agui 的方向,不做。

中立消息 → AG-UI 消息。

`:id` **优先用消息自己的**——ChatMemory 落库时补的那个(`msg/ensure-id`),
跨快照稳定。没有才按位置合成(`m-<idx>`):那只保证「同一次快照内部自洽」,
`replace-tool-results` / `heal-dangling` 一让位置漂,同一条消息在两次快照里
就是两个 id。走内置 store 的都有 id,退路是留给第三方 store 的。

**与事件流的 message-id 不是一个 id 空间**(那边是 `<run-id>-mN`)——合成一个
要让发射器的 id 流进 `response->neutral`,那是 core 依赖 agui 的方向,不做。
sourceraw docstring

messages->aguiclj

(messages->agui messages)
source

messages->thread-messagesclj

(messages->thread-messages messages)

中立消息 → /threads/:id/messages 的形状。

与事件流里的 message->agui 不是一个形状(上游也分两套):这里的 toolCalls 是扁的 {id, name, args},事件流那套是 OpenAI 风格的 {id, type, function:{name, arguments}}。照抄上游 handleGetThreadMessages 的本地分支——线程列表页读的是这一套。

中立消息 → `/threads/:id/messages` 的形状。

**与事件流里的 `message->agui` 不是一个形状**(上游也分两套):这里的
`toolCalls` 是**扁的** `{id, name, args}`,事件流那套是 OpenAI 风格的
`{id, type, function:{name, arguments}}`。照抄上游 `handleGetThreadMessages`
的本地分支——线程列表页读的是这一套。
sourceraw docstring

parse-run-inputclj

(parse-run-input {:keys [threadId runId messages tools state context
                         forwardedProps resume parentRunId]})

AG-UI RunAgentInput → 我们的 run 参数。

历史取服务端权威(§7.3):客户端带上来的 messages 是它自己那份完整历史, 而我们的 memory filter 是服务端权威,且循环内落库 / heal-dangling / 暂停恢复 / timeline 四样全依赖它。所以这里只取最后一条 user 消息,其余忽略;前端要 对齐历史走 connect 的 MESSAGES_SNAPSHOT。

state 作为本 run 的初始 context 注入——turn 级、不落库,与现有语义逐字 一致(跨 run 共享状态见 §7.1,路径是 fold-from-history,不建快照店)。

resume 是 interrupt 协议的答复数组({interruptId, status, payload}): 上一条 run 以 outcome:"interrupt" 收口,客户端把人的决定放在下一次 run 的请求体里带回来——协议里没有单独的审批端点,就是这一条。

context 是前端注册的本轮环境说明(useAgentContext,每次 run 都随请求 体重发)——渲染成 system 段的活在 context->prompt。

parentRunId 是可选的父 run 声明(嵌套 run)。我们不解释它,只原样回到 RUN_STARTED.parentRunId 上。⚠️ CopilotKit 的 JS 客户端在发出去之前会把这个 字段解构掉(@ag-ui/client 的 dist 里对它只有一处引用,就是丢弃那处), 所以走 CopilotKit 这条路它恒为 nil——留着是为别的 AG-UI SDK 与自建客户端。

AG-UI `RunAgentInput` → 我们的 run 参数。

**历史取服务端权威**(§7.3):客户端带上来的 `messages` 是它自己那份完整历史,
而我们的 memory filter 是服务端权威,且循环内落库 / heal-dangling / 暂停恢复 /
timeline 四样全依赖它。所以这里**只取最后一条 user 消息**,其余忽略;前端要
对齐历史走 `connect` 的 `MESSAGES_SNAPSHOT`。

`state` 作为本 run 的**初始 context** 注入——turn 级、不落库,与现有语义逐字
一致(跨 run 共享状态见 §7.1,路径是 fold-from-history,不建快照店)。

`resume` 是 interrupt 协议的答复数组(`{interruptId, status, payload}`):
上一条 run 以 `outcome:"interrupt"` 收口,客户端把人的决定放在**下一次 run
的请求体**里带回来——协议里没有单独的审批端点,就是这一条。

`context` 是前端注册的**本轮环境说明**(`useAgentContext`,每次 run 都随请求
体重发)——渲染成 system 段的活在 `context->prompt`。

`parentRunId` 是可选的父 run 声明(嵌套 run)。我们**不解释它**,只原样回到
`RUN_STARTED.parentRunId` 上。⚠️ CopilotKit 的 JS 客户端在发出去之前会把这个
字段解构掉(`@ag-ui/client` 的 dist 里对它只有一处引用,就是丢弃那处),
所以走 CopilotKit 这条路它恒为 nil——留着是为别的 AG-UI SDK 与自建客户端。
sourceraw docstring

run-infoclj

(run-info agent-ids)
(run-info agent-ids
          {:keys [version descriptions capabilities open-generative-ui? a2ui?
                  suggestions? threads? thread-endpoints multimodal
                  parallel-tools? max-iterations state? approve-with-edits?]})

GET {base}/info 的响应体。

agents 是以 agent id 为键的字典,不是数组——客户端拿它做 Object.entries(runtimeInfo.agents) 再按 id 建 proxy(agent-registry.ts)。 给数组不会报错,只会把下标当成 agent id,于是前端去请求 /agent/0/run。 这条是联调时才照出来的:单测和 live 脚本都不碰 /info。

只声明我们真支持的——不谎报能力位(intelligence / inspectorMetadata 是 CopilotKit 云产品的东西,我们没有就是没有;不写这些键等于告诉客户端 「没有」,它会走降级路径)。

className 客户端不解释,只在调试面板显示。

capabilities 是每个 agent 一份(客户端 agent-registry 从这里读, 再交给 useCapabilities)。我们报的这三位都是真的:会暂停等人 (supported)、敏感工具要审批(approvals)、走 AG-UI 的 interrupt 协议 (interrupts:RUN_FINISHED outcome=interrupt + 收 resume[])。 approveWithEdits 由装配方报(:approve-with-edits?,缺省 false): 改参数再执行要路由把 editedArgs 翻成 resume 的 {:args}——这层判不了 路由接没接,所以不替它说(examples 的路由接了,缺省报 true)。

multimodal 是装配方传进来的,本 ns 不猜:能不能收图取决于两件事—— wire 认不认部件(wire/anthropic / wire/openai 都认)且模型本身有视觉, 而后一条 codec 这层判不了。⛔ 也别按「provider 是谁」硬编码:同一个 provider 下 MiniMax-M2.7 没视觉、qwen-vl 有。不传就不报这一格——客户端据此把附件 入口收起来,比摆一颗点了才发现没用的回形针强。

`GET {base}/info` 的响应体。

**`agents` 是以 agent id 为键的字典,不是数组**——客户端拿它做
`Object.entries(runtimeInfo.agents)` 再按 id 建 proxy(`agent-registry.ts`)。
给数组不会报错,只会把下标当成 agent id,于是前端去请求 `/agent/0/run`。
这条是**联调时才照出来的**:单测和 live 脚本都不碰 `/info`。

只声明我们真支持的——**不谎报能力位**(`intelligence` / `inspectorMetadata`
是 CopilotKit 云产品的东西,我们没有就是没有;不写这些键等于告诉客户端
「没有」,它会走降级路径)。

`className` 客户端不解释,只在调试面板显示。

`capabilities` 是**每个 agent 一份**(客户端 `agent-registry` 从这里读,
再交给 `useCapabilities`)。我们报的这三位都是真的:会暂停等人
(`supported`)、敏感工具要审批(`approvals`)、走 AG-UI 的 interrupt 协议
(`interrupts`:`RUN_FINISHED outcome=interrupt` + 收 `resume[]`)。
`approveWithEdits` **由装配方报**(`:approve-with-edits?`,缺省 false):
改参数再执行要路由把 `editedArgs` 翻成 resume 的 `{:args}`——这层判不了
路由接没接,所以不替它说(examples 的路由接了,缺省报 true)。

`multimodal` 是**装配方传进来的**,本 ns 不猜:能不能收图取决于两件事——
wire 认不认部件(`wire/anthropic` / `wire/openai` 都认)**且模型本身有视觉**,
而后一条 codec 这层判不了。⛔ 也别按「provider 是谁」硬编码:同一个 provider
下 `MiniMax-M2.7` 没视觉、`qwen-vl` 有。不传就不报这一格——客户端据此把附件
入口收起来,比摆一颗点了才发现没用的回形针强。
sourceraw docstring

run-input-problemclj

(run-input-problem body)
(run-input-problem body {:keys [require-run-id?]})

校验 RunAgentInput 的最小形状。返回 nil(没问题)或 {:error <机器可读的码> :message <给人看的话>}。

为什么要有:不校验时坏输入一路滑到 start-run!——缺 threadId 就拿 nil 当 会话 id 照跑(一个所有人共享的 nil 会话),坏 JSON 则在路由里炸成 500,客户端 分不清是自己错了还是服务挂了(designs/agui-borrow.md §7)。

只出数据,不出状态码:码怎么映射成 400 是路由的事(本 ns 不产出 HTTP, 见 ns docstring)。body 是已解析的请求体(解析失败的那一步在路由里, 它同样回 bad_json)。

  • :require-run-id? —— /run 要(AG-UI 的 RunAgentInput.runId 必填), /connect 只认 threadId
校验 `RunAgentInput` 的**最小形状**。返回 nil(没问题)或
`{:error <机器可读的码> :message <给人看的话>}`。

为什么要有:不校验时坏输入一路滑到 `start-run!`——缺 `threadId` 就拿 nil 当
会话 id 照跑(一个所有人共享的 nil 会话),坏 JSON 则在路由里炸成 500,客户端
分不清是自己错了还是服务挂了(designs/agui-borrow.md §7)。

**只出数据,不出状态码**:码怎么映射成 400 是路由的事(本 ns 不产出 HTTP,
见 ns docstring)。`body` 是**已解析**的请求体(解析失败的那一步在路由里,
它同样回 `bad_json`)。

- `:require-run-id?` —— `/run` 要(AG-UI 的 `RunAgentInput.runId` 必填),
  `/connect` 只认 `threadId`
sourceraw docstring

terminal?clj

(terminal? agui-event)

这条 AG-UI 事件是不是终态——传输层据此关流。AG-UI 的 run 必须以终态收口, 收不到终态的客户端会一直吊着(联调时的表现:工具卡片永远停在 inProgress)。

这条 AG-UI 事件是不是**终态**——传输层据此关流。AG-UI 的 run 必须以终态收口,
收不到终态的客户端会一直吊着(联调时的表现:工具卡片永远停在 inProgress)。
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close