共享状态的 JSON Pointer / JSON Patch(RFC 6901 / 6902)与 delta 规范化。
纯函数,不认识发射器也不认识 AG-UI 事件——拿状态与 op 数组进来,算出新
状态与(可能被补过的)op 数组出去。事件那一侧在 agui.event。
为什么服务端非得自己算一遍:AG-UI 的共享状态是「一条快照 + 一串增量」,增量
打在客户端那份状态上。服务端每发一条 delta 都得同步在自己这边应用,否则
下一条 delta 就是对着一个想象中的状态算的。上游把这件事放在服务端解
(CopilotKit/packages/runtime/src/agent/state-delta.ts),我们照同一个口径。
normalize-ops 挡的是一个一个字都不报的静默失败(下游在另一台运行时上
实测到,见 feedbacks/2026-09-04-agui-no-shared-state-tools-or-delta.md):
客户端 state 还是 {} 时收到 add /todos/-——RFC 6902 要求 /todos 已存在,
必然失败,而客户端只保留旧状态 + console.warn,一个错都不报给用户。
「没发过快照就先发 delta」那条由 agui.event 在发射侧补(它才知道发过没有)。
模型写错了当作「这条 op 不适用」跳过,不抛——那是模型生成的字符串, 炸掉整条 run 比丢一条 op 糟得多。
共享状态的 JSON Pointer / JSON Patch(RFC 6901 / 6902)与 **delta 规范化**。
**纯函数,不认识发射器也不认识 AG-UI 事件**——拿状态与 op 数组进来,算出新
状态与(可能被补过的)op 数组出去。事件那一侧在 `agui.event`。
为什么服务端非得自己算一遍:AG-UI 的共享状态是「一条快照 + 一串增量」,增量
**打在客户端那份状态上**。服务端每发一条 delta 都得同步在自己这边应用,否则
下一条 delta 就是对着一个想象中的状态算的。上游把这件事放在服务端解
(`CopilotKit/packages/runtime/src/agent/state-delta.ts`),我们照同一个口径。
`normalize-ops` 挡的是一个**一个字都不报**的静默失败(下游在另一台运行时上
实测到,见 feedbacks/2026-09-04-agui-no-shared-state-tools-or-delta.md):
客户端 `state` 还是 `{}` 时收到 `add /todos/-`——RFC 6902 要求 `/todos` 已存在,
必然失败,而客户端**只保留旧状态 + `console.warn`**,一个错都不报给用户。
「没发过快照就先发 delta」那条由 `agui.event` 在发射侧补(它才知道发过没有)。
**模型写错了当作「这条 op 不适用」跳过,不抛**——那是模型生成的字符串,
炸掉整条 run 比丢一条 op 糟得多。(apply-op root op)应用一条 op,返回 {:root r :applied? bool}。
覆盖 add / remove / replace / test / copy / move(与上游
applyStateDeltaOperation 同集);认不出的 op、非法路径一律 :applied? false
且状态原样。
应用一条 op,返回 `{:root r :applied? bool}`。
覆盖 `add` / `remove` / `replace` / `test` / `copy` / `move`(与上游
`applyStateDeltaOperation` 同集);认不出的 op、非法路径一律 `:applied? false`
且状态原样。(apply-ops root ops)顺序应用一串 op。跳过不适用的,不抛(同 apply-op 的理由)。
顺序应用一串 op。**跳过不适用的**,不抛(同 `apply-op` 的理由)。
(lookup root segs)顺路径取值,返回 {:exists? bool :value v}。
存在与「值是 nil」必须分开——补数组那条守卫全靠这个区分。
顺路径取值,返回 `{:exists? bool :value v}`。
**存在与「值是 nil」必须分开**——补数组那条守卫全靠这个区分。(normalize-ops ops state)规范化一串 op,返回 {:ops [...] :state 新状态}。
唯一的补丁动作(照抄上游 normalizeStateDelta):add /x/- 而 /x 不存在、
但 /x 的父存在时,在它前面插一条 {op:add, path:/x, value:[]}。
为什么必须在服务端补:RFC 6902 的 add /todos/- 要求 /todos 已经是个数组;
客户端 state 还是 {} 时这条必然失败,而 @ag-ui/client 只保留旧状态 +
console.warn——用户看到的是「什么都没发生」,没有任何报错指向真正的原因。
父不存在时不补(那要连着建好几层,猜得太多)——照发,让它按 RFC 失败, 与上游行为一致。
规范化一串 op,返回 `{:ops [...] :state 新状态}`。
唯一的补丁动作(照抄上游 `normalizeStateDelta`):`add /x/-` 而 `/x` **不存在**、
但 `/x` 的父存在时,**在它前面插一条 `{op:add, path:/x, value:[]}`**。
为什么必须在服务端补:RFC 6902 的 `add /todos/-` 要求 `/todos` 已经是个数组;
客户端 state 还是 `{}` 时这条必然失败,而 `@ag-ui/client` **只保留旧状态 +
console.warn**——用户看到的是「什么都没发生」,没有任何报错指向真正的原因。
父不存在时**不补**(那要连着建好几层,猜得太多)——照发,让它按 RFC 失败,
与上游行为一致。(parse-pointer p)"/a/b" → ["a" "b"];"" → [];非法返回 nil(不抛)。
非法有两种:不以 / 开头的非空串;~ 后面不是 0/1(转义只定义了
~0 = ~、~1 = /)。
`"/a/b"` → `["a" "b"]`;`""` → `[]`;**非法返回 nil**(不抛)。 非法有两种:不以 `/` 开头的非空串;`~` 后面不是 `0`/`1`(转义只定义了 `~0` = `~`、`~1` = `/`)。
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 |