Liking cljdoc? Tell your friends :D

im.ttalk.agent.agui.event

中立事件模型 + 每 run 的发射器。

零 AG-UI 概念(协议编码在 agui.codec),零 web 概念 (design-principles §2:本模块不认识 Request/Response/SSE 帧)。

三条契约(docs/agent-runtime-design.md §4.2):

  1. :seq 在一个会话内单调递增、无洞——不是每 run 重置。HITL 下一次对话 由多个 run 组成(run → :run/paused → resume 起 run'),订阅挂在会话上跨 run 连续;seq 按 run 重置会让 :since 退化成 {run-id, seq} 二元组。 故 next-seq 由会话提供,发射器只管取号。
  2. 恰好一个终态事件,且它是该 run 的最后一个事件。finish! 是终态的唯一 出口且幂等——「已有终态就什么都不补」这条规则因此由构造保证,而不是靠检查。
  3. 错误值是 canonical error(model.error),不是字符串。

发射器自身永不抛(§6.3):callbacks/invoke 的吞异常语义对观察类回调是 对的,对事件流是错的——吞掉一次发射 = :seq 出洞 = 断线续传从此对不上, 而且不报错。所以这里反过来:sink 抛异常由发射器兜住并计数, :seq 由发射器自己分配(不依赖 sink 的返回值),一次失败不影响后续编号。

中立事件模型 + 每 run 的发射器。

**零 AG-UI 概念**(协议编码在 `agui.codec`),**零 web 概念**
(design-principles §2:本模块不认识 Request/Response/SSE 帧)。

三条契约(docs/agent-runtime-design.md §4.2):

1. `:seq` 在一个**会话**内单调递增、无洞——不是每 run 重置。HITL 下一次对话
   由多个 run 组成(run → `:run/paused` → resume 起 run'),订阅挂在会话上跨
   run 连续;seq 按 run 重置会让 `:since` 退化成 `{run-id, seq}` 二元组。
   故 `next-seq` 由会话提供,发射器只管取号。
2. **恰好一个终态事件**,且它是该 run 的最后一个事件。`finish!` 是终态的唯一
   出口且幂等——「已有终态就什么都不补」这条规则因此由构造保证,而不是靠检查。
3. 错误值是 canonical error(`model.error`),不是字符串。

**发射器自身永不抛**(§6.3):`callbacks/invoke` 的吞异常语义对观察类回调是
对的,对事件流是错的——吞掉一次发射 = `:seq` 出洞 = 断线续传从此对不上,
而且不报错。所以这里反过来:sink 抛异常由**发射器**兜住并计数,
`:seq` 由发射器自己分配(不依赖 sink 的返回值),一次失败不影响后续编号。
raw docstring

agent-stateclj

(agent-state em)

本 run 的共享状态当前值(服务端这份)。

本 run 的共享状态**当前值**(服务端这份)。
sourceraw docstring

begin-message!clj

(begin-message! em message-id)

声明「接下来的 token 属于这条消息」。不发事件——真到第一个 token 才发 :message/started,免得没产出文本的一轮凭空多一条空消息。

声明「接下来的 token 属于这条消息」。**不发事件**——真到第一个 token 才发
`:message/started`,免得没产出文本的一轮凭空多一条空消息。
sourceraw docstring

current-messageclj

(current-message em)
source

dropsclj

(drops em)
source

emit!clj

(emit! em type m)

发一个事件。返回最后一条真正发出的事件(transform 把它吞了则返回 nil; sink 抛异常时仍返回,只计一次 drop)。

永不抛:这是 §6.3 立的不变量,有测试钉住。

发一个事件。返回**最后一条真正发出的**事件(transform 把它吞了则返回 nil;
sink 抛异常时仍返回,只计一次 drop)。

**永不抛**:这是 §6.3 立的不变量,有测试钉住。
sourceraw docstring

emit-state-delta!clj

(emit-state-delta! em ops)

规范化 + 发 :state/delta(RFC 6902 op 数组)。返回实际发出去的 op 数组。

两件在这里解掉的事,都是一个字都不报的静默失败:

  1. 没发过快照就发 delta —— 客户端没有基线,patch 无处可打。所以首条 delta 之前先补一条当前状态的快照。

    ⛔ 「没有状态」补的是 {},不是跳过 —— 这一条是刻意比上游严,别「对齐 上游」把它改回去。 上游那个守卫逐字是 !hasEmittedState && initialState !== undefined (CopilotKit/packages/runtime/src/agent/state-delta.ts 的 createStateEventNormalizer):state 字段缺席时它不补快照。上游 自己撞不到,是因为它的 AbstractAgent 永远发 state ?? {};而我们这条路 客户端可以真的不带这个字段。跳过的后果不是少一条快照,是下面第 2 条守卫 一起失灵——对着 nil 算,连 add /todos [] 这条初始化 op 自己都打不上, 于是裸 delta 照样发出去,客户端 console.warn 一声就把它丢了。 我们把「字段缺席」与 {} 当同一件事(客户端的 state 缺省本来就是 {})。 下游在 2026-09-04 的回执里专门记了这一格,说另一台参照实现照抄上游的条件、 那条路上仍会发出打不上的裸 delta;

  2. add /x/- 而 /x 还不存在 —— 由 state/normalize-ops 在前面插一条 add /x [](见那个 ns 的注释)。

服务端这边同步应用一遍,下一条 delta 才是对着真实值算的。

规范化 + 发 `:state/delta`(RFC 6902 op 数组)。返回**实际发出去的** op 数组。

两件在这里解掉的事,都是**一个字都不报**的静默失败:

1. **没发过快照就发 delta** —— 客户端没有基线,patch 无处可打。所以首条 delta
   之前先补一条当前状态的快照。

   ⛔ **「没有状态」补的是 `{}`,不是跳过 —— 这一条是刻意比上游严,别「对齐
   上游」把它改回去。** 上游那个守卫逐字是
   `!hasEmittedState && initialState !== undefined`
   (`CopilotKit/packages/runtime/src/agent/state-delta.ts`
   的 `createStateEventNormalizer`):`state` 字段**缺席**时它不补快照。上游
   自己撞不到,是因为它的 `AbstractAgent` 永远发 `state ?? {}`;而我们这条路
   客户端可以真的不带这个字段。跳过的后果不是少一条快照,是**下面第 2 条守卫
   一起失灵**——对着 nil 算,连 `add /todos []` 这条初始化 op 自己都打不上,
   于是裸 delta 照样发出去,客户端 `console.warn` 一声就把它丢了。
   我们把「字段缺席」与 `{}` 当同一件事(客户端的 state 缺省本来就是 `{}`)。
   下游在 2026-09-04 的回执里专门记了这一格,说另一台参照实现照抄上游的条件、
   那条路上仍会发出打不上的裸 delta;
2. **`add /x/-` 而 `/x` 还不存在** —— 由 `state/normalize-ops` 在前面插一条
   `add /x []`(见那个 ns 的注释)。

服务端这边同步应用一遍,下一条 delta 才是对着真实值算的。
sourceraw docstring

emit-state-snapshot!clj

(emit-state-snapshot! em v)

整份替换 + 发 :state/snapshot。

⚠️ v 必须是状态对象本身,不是 JSON 字符串。发成字符串之后客户端那边是 一坨转义引号,更要命的是后续每条 delta 都打不上(patch 打在字符串上), 而上游客户端不防这一手(state = snapshot 直接替换)。这里做不了类型强制 ——JSON 里字符串也是合法值——但调用方(agui.tools 的写状态工具)会先解一层。

整份替换 + 发 `:state/snapshot`。

⚠️ `v` 必须是**状态对象本身,不是 JSON 字符串**。发成字符串之后客户端那边是
一坨转义引号,更要命的是**后续每条 delta 都打不上**(patch 打在字符串上),
而上游客户端不防这一手(`state = snapshot` 直接替换)。这里做不了类型强制
——JSON 里字符串也是合法值——但调用方(`agui.tools` 的写状态工具)会先解一层。
sourceraw docstring

emit-token!clj

(emit-token! em kind text)

增量 token。kind = :token(正文)或 :reasoning-token(思维)。

两条独立的消息,各自开合:正文首个 token 补 :message/started,思维首个 token 补 :reasoning/started;互相到达时把对方收口(一轮里可以来回切换, 模型边想边说是常态)。

增量 token。`kind` = `:token`(正文)或 `:reasoning-token`(思维)。

两条独立的消息,各自开合:正文首个 token 补 `:message/started`,思维首个
token 补 `:reasoning/started`;**互相到达时把对方收口**(一轮里可以来回切换,
模型边想边说是常态)。
sourceraw docstring

emitterclj

(emitter {:keys [run-id conversation-id next-seq sink now transform tag gate]})

构造一个 run 的发射器。

  • :run-id 本 run 的 id
  • :conversation-id 会话 id
  • :next-seq (fn [] long)——会话级取号器(契约 1)
  • :sink (fn [event])——事件出口(runtime 提供:入缓冲 + 扇出)
  • :now (fn [] ms),可注入(测试)
  • :transform (fn [event]) → 事件序列,可选。插件的挂点:可以改写、 吞掉、或在一条事件前后插入别的事件(copilotkit.genui 就靠它把 工具调用翻译成 activity 事件)。每个产出各自取号,所以 契约 1 不受影响;终态事件不过 transform(契约 2 因此 仍由构造保证);transform 抛异常 = 原样放行并记一条 warn

返回一个 map(内部状态在 atom 里)。停止意图挂在这里而不是会话上: stop! 与 supersede 改的都是被停的那个 run 自己的 holder,否则新 run 一 重置共享标志,被替换掉的旧 run 收尾时就会把自己标成 error——CopilotKit 的 RunFinalizeControl 修的正是这个(§2.1 第 4 条)。

  • :tag 可选。merge 进本发射器发出的每一条事件(m 优先)。 子 agent lane 的归属字段就是这么打上去的,见 subagent-emitter;run 自己的发射器不带 tag。
构造一个 run 的发射器。

- :run-id          本 run 的 id
- :conversation-id 会话 id
- :next-seq        (fn [] long)——会话级取号器(契约 1)
- :sink            (fn [event])——事件出口(runtime 提供:入缓冲 + 扇出)
- :now             (fn [] ms),可注入(测试)
- :transform       (fn [event]) → **事件序列**,可选。**插件的挂点**:可以改写、
                   吞掉、或在一条事件前后插入别的事件(`copilotkit.genui` 就靠它把
                   工具调用翻译成 activity 事件)。每个产出各自取号,所以
                   契约 1 不受影响;**终态事件不过 transform**(契约 2 因此
                   仍由构造保证);transform 抛异常 = 原样放行并记一条 warn

返回一个 map(内部状态在 atom 里)。**停止意图挂在这里而不是会话上**:
`stop!` 与 supersede 改的都是**被停的那个 run** 自己的 holder,否则新 run 一
重置共享标志,被替换掉的旧 run 收尾时就会把自己标成 error——CopilotKit 的
`RunFinalizeControl` 修的正是这个(§2.1 第 4 条)。

- :tag              可选。**merge 进本发射器发出的每一条事件**(`m` 优先)。
                    子 agent lane 的归属字段就是这么打上去的,见
                    `subagent-emitter`;run 自己的发射器不带 tag。
sourceraw docstring

end-message!clj

(end-message! em)
(end-message! em full-text)

关掉当前消息(若开着)。full-text 非空且该消息一个 token 都没出过时, 补一条整块 delta——非流式 run 的最终答案就是这么进事件流的。

关掉当前消息(若开着)。`full-text` 非空且该消息**一个 token 都没出过**时,
补一条整块 delta——非流式 run 的最终答案就是这么进事件流的。
sourceraw docstring

end-reasoning!clj

(end-reasoning! em)

关掉开着的思考块(若有)。正文 token 一到就得关:模型「想完了开始说」, AG-UI 侧就是思考消息收口、正文消息开始。

关掉开着的思考块(若有)。**正文 token 一到就得关**:模型「想完了开始说」,
AG-UI 侧就是思考消息收口、正文消息开始。
sourceraw docstring

ensure-text!clj

(ensure-text! em message-id text)

收尾兜底:本 run 一个字都没出过、但结果里有文本时,把它补成一条完整消息。

踩得到的路径:return-direct(最终答案由工具结果合成,没有第二次 LLM 文本 往返),以及任何在 :on-llm-result 之外产生最终文本的收尾。

收尾兜底:本 run 一个字都没出过、但结果里有文本时,把它补成一条完整消息。

踩得到的路径:`return-direct`(最终答案由工具结果合成,没有第二次 LLM 文本
往返),以及任何在 `:on-llm-result` 之外产生最终文本的收尾。
sourceraw docstring

finish!clj

(finish! em type m)

终态的唯一出口,幂等:第一次调用补关开着的块并发终态,之后一律 no-op。

§4.6 的「已有终态则什么都不补」因此是构造保证的——不是靠在每个发射点检查。

顺序:先收子 lane,再关自己的块,最后发终态。lane 的收尾属于父 run 终态 之前的事件;反过来先发终态,lane 就被静音了(见 close-lanes!)。

终态的**唯一出口**,幂等:第一次调用补关开着的块并发终态,之后一律 no-op。

§4.6 的「已有终态则什么都不补」因此是构造保证的——不是靠在每个发射点检查。

顺序:**先收子 lane,再关自己的块,最后发终态**。lane 的收尾属于父 run 终态
之前的事件;反过来先发终态,lane 就被静音了(见 `close-lanes!`)。
sourceraw docstring

finish-subagent!clj

(finish-subagent! em {:keys [outcome error result interrupt-ids]})

lane 收尾的唯一出口,幂等:补关 lane 自己开着的块,再发一条收尾事件。

m = {:outcome :success | :killed | :timeout :result <any>} 或 {:error <canonical error>}。有 :error 走 :subagent/error,否则走 :subagent/finished;:result 是协议里那个可选的完成载荷 (SUBAGENT_FINISHED.result,语义同 RUN_FINISHED.result)。

这两个类型都不进 terminal-types:那个集合是 run 的终态,expand 靠它决定 「终态不过 transform」。lane 的收尾必须过 transform——它得受父 run 终态守卫的管, 否则父 run 收口之后还能补出一条 SUBAGENT_FINISHED。这里的幂等靠自己标记。

lane 收尾的**唯一出口**,幂等:补关 lane 自己开着的块,再发一条收尾事件。

`m` = `{:outcome :success | :killed | :timeout :result <any>}` 或
`{:error <canonical error>}`。有 `:error` 走 `:subagent/error`,否则走
`:subagent/finished`;`:result` 是协议里那个可选的完成载荷
(`SUBAGENT_FINISHED.result`,语义同 `RUN_FINISHED.result`)。

**这两个类型都不进 `terminal-types`**:那个集合是 run 的终态,`expand` 靠它决定
「终态不过 transform」。lane 的收尾必须过 transform——它得受父 run 终态守卫的管,
否则父 run 收口之后还能补出一条 `SUBAGENT_FINISHED`。这里的幂等靠自己标记。
sourceraw docstring

lane-idclj

(lane-id em)

本发射器的 lane id;run 自己的发射器为 nil。消息 id 要靠它分道(契约 4)。

本发射器的 lane id;run 自己的发射器为 nil。消息 id 要靠它分道(契约 4)。
sourceraw docstring

record-usage!clj

(record-usage! em m)

记一次 LLM 往返的用量。不发事件——它在终态那条上一次性带出去。

m 形如 {:model "…" :provider "…" :input-tokens n :output-tokens k :total-tokens t :cache-read-tokens c}(中立命名,response/normalize-usage 的形状);译成 AG-UI 的 inputTokens/outputTokens/… 是 codec 的事。

全空(provider 没报 usage)就不记——宁可不给这一格,也不给一排 0 让客户端 把「没数据」画成「用了 0 个 token」。

记一次 LLM 往返的用量。**不发事件**——它在终态那条上一次性带出去。

`m` 形如 `{:model "…" :provider "…" :input-tokens n :output-tokens k
:total-tokens t :cache-read-tokens c}`(中立命名,`response/normalize-usage`
的形状);译成 AG-UI 的 `inputTokens`/`outputTokens`/… 是 codec 的事。

全空(provider 没报 usage)就不记——宁可不给这一格,也不给一排 0 让客户端
把「没数据」画成「用了 0 个 token」。
sourceraw docstring

request-stop!clj

(request-stop! em)
(request-stop! em reason)

登记本 run 的停止意图(stop! / supersede / shutdown / 孤儿都走这里)。 返回是否是首次登记。

reason 缺省 :stop;第一个原因算数——先按停、随后停机,终态上仍写 :stop(用户的意图先到,停机只是没等它收完)。

登记**本 run** 的停止意图(`stop!` / supersede / shutdown / 孤儿都走这里)。
返回是否是首次登记。

`reason` 缺省 `:stop`;**第一个原因算数**——先按停、随后停机,终态上仍写
`:stop`(用户的意图先到,停机只是没等它收完)。
sourceraw docstring

seed-state!clj

(seed-state! em v)

把 RunAgentInput.state 作为本 run 的初始状态装上但不发。

客户端本来就有这份(是它发上来的),再发一遍是噪声;装上是为了后续 delta 对着真实基线算,而不是对着 nil 算。

把 `RunAgentInput.state` 作为本 run 的初始状态**装上但不发**。

客户端本来就有这份(是它发上来的),再发一遍是噪声;装上是为了后续 delta
对着**真实基线**算,而不是对着 nil 算。
sourceraw docstring

silenced-countclj

(silenced-count em)

本 lane 因父 run(或祖先 lane)已终态而被吞掉的事件数。

与 drops(sink 抛异常)分开计:一个是「出口坏了」,一个是「说话的时机过了」, 排查时是两码事。

本 lane 因父 run(或祖先 lane)已终态而被吞掉的事件数。

与 `drops`(sink 抛异常)分开计:一个是「出口坏了」,一个是「说话的时机过了」,
排查时是两码事。
sourceraw docstring

silenced?clj

(silenced? em)

本发射器是否该闭嘴了——自己已有终态,或任一祖先(父 lane / 父 run)已有终态。

只有 lane 会有祖先(:parent)。递归而不是只看直接父级:两层委派下,run 收口时 内层 lane 的直接父级(外层 lane)可能还没收口,只看一级就会让内层的事件漏在 RUN_FINISHED 之后——契约 2「终态是该 run 的最后一条」当场破。

本发射器是否该闭嘴了——自己已有终态,或**任一祖先**(父 lane / 父 run)已有终态。

只有 lane 会有祖先(`:parent`)。递归而不是只看直接父级:两层委派下,run 收口时
内层 lane 的直接父级(外层 lane)可能还没收口,只看一级就会让内层的事件漏在
`RUN_FINISHED` 之后——契约 2「终态是该 run 的最后一条」当场破。
sourceraw docstring

start-subagent!clj

(start-subagent! em {:keys [name task parent-tool-call-id parent-message-id]})

lane 的开场。:subagent-run-id 由 tag 自动带上,这里只补 lane 特有的字段。

parent-tool-call-id 由委派工具从 ToolContext 的 :tool/call-id 取 (react/invoke-one 钉的)。同一批里几个委派并发时,这是唯一能把 lane 挂回 正确那张工具卡片的依据——从「开着的工具」里猜,多路并发下必然挂错。 协议里它是可选字段,取不到就空着。

lane 的开场。`:subagent-run-id` 由 tag 自动带上,这里只补 lane 特有的字段。

`parent-tool-call-id` 由委派工具从 ToolContext 的 `:tool/call-id` 取
(`react/invoke-one` 钉的)。同一批里几个委派并发时,这是唯一能把 lane 挂回
正确那张工具卡片的依据——从「开着的工具」里猜,多路并发下必然挂错。
协议里它是可选字段,取不到就空着。
sourceraw docstring

started?clj

(started? em)

本 lane 是否宣告过自己(发过 :subagent/started)。run 自己的发射器恒为 false。

本 lane 是否宣告过自己(发过 `:subagent/started`)。run 自己的发射器恒为 false。
sourceraw docstring

stop-reasonclj

(stop-reason em)

本 run 的取消原因(没登记过取消 = nil)。

本 run 的取消原因(没登记过取消 = nil)。
sourceraw docstring

stop-reasonsclj

取消的四种原因(designs/agui-borrow.md §5,对照 Keelson AguiCancelReason):

原因谁触发
:stop用户按停(runtime/stop! 缺省)
:superseded:on-concurrent :supersede 被同会话的新 run 顶掉
:shutdownruntime/shutdown!
:orphan最后一个订阅者走后 :orphan-cancel-ms 到点仍没人来
取消的四种原因(designs/agui-borrow.md §5,对照 Keelson `AguiCancelReason`):

| 原因 | 谁触发 |
|---|---|
| `:stop`       | 用户按停(`runtime/stop!` 缺省) |
| `:superseded` | `:on-concurrent :supersede` 被同会话的新 run 顶掉 |
| `:shutdown`   | `runtime/shutdown!` |
| `:orphan`     | 最后一个订阅者走后 `:orphan-cancel-ms` 到点仍没人来 |
sourceraw docstring

stop-requested?clj

(stop-requested? em)
source

subagent-emitterclj

(subagent-emitter parent {:keys [subagent-run-id]})

给 parent(run 的发射器,或另一条 lane)派生一条 lane 的发射器。

:transform 槽被终态守卫占用,所以 lane 不接插件——插件 transform 是按 run 现造的有状态对象,多条 lane 共用一个实例,它记的「这一轮见过哪些 tool-call」会被 交错的 lane 污染。

给 `parent`(run 的发射器,或另一条 lane)派生一条 lane 的发射器。

`:transform` 槽被终态守卫占用,所以 lane **不接插件**——插件 transform 是按 run
现造的有状态对象,多条 lane 共用一个实例,它记的「这一轮见过哪些 tool-call」会被
交错的 lane 污染。
sourceraw docstring

terminal-ofclj

(terminal-of em)
source

terminal-typesclj

终态事件类型。:run/paused 也是终态——暂停是 run 的结束,续跑是新的 run (docs/agent-runtime-design.md §4.7)。

终态事件类型。`:run/paused` 也是终态——暂停是 run 的结束,续跑是**新的 run**
(docs/agent-runtime-design.md §4.7)。
sourceraw docstring

terminal?clj

(terminal? event)
source

text-emitted?clj

(text-emitted? em)

本 run 是否已经产出过任何文本(流式 token 或整块补发都算)。

本 run 是否已经产出过任何文本(流式 token 或整块补发都算)。
sourceraw docstring

tool-parent-messageclj

(tool-parent-message em tool-call-id)

某次工具调用所属的正文消息 id(:tool/started 时记下的)。

工具还开着(没到 :tool/result)才查得到——子 agent 的 start! 正是在工具 执行期间调的,所以够用。

某次工具调用所属的正文消息 id(`:tool/started` 时记下的)。

工具还开着(没到 `:tool/result`)才查得到——子 agent 的 `start!` 正是在工具
执行期间调的,所以够用。
sourceraw docstring

usage-ofclj

(usage-of em)

本 run 至今记下的用量条目(根发射器上的那份)。

本 run 至今记下的用量条目(根发射器上的那份)。
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