Liking cljdoc? Tell your friends :D

im.ttalk.agent.server.adapter.http-kit

http-kit 的垫片:让它也能发 Ring SPEC 的流式正文(ring.core.protocols/StreamableResponseBody)。

http-kit 2.8 的 adapter 不认识 SPEC 的流式正文(直接 500「not understandable」),惰性 seq / 管道 InputStream 正文 它也是攒满了才发(designs/server-module.md §1 的实测)。本中间件把这样的响应改走 http-kit 自己的 as-channel: 先发状态码与头,再在一条虚拟线程上 write-body-to-stream 到一个转发给 send! 的 OutputStream,每次 write 即推送。

(require '[org.httpkit.server :as hk]
         '[im.ttalk.agent.server.adapter.http-kit :as hk-shim])
(hk/run-server (hk-shim/wrap-streaming handler) {:port 3000})
  • 只改直接实现 StreamableResponseBody 接口的正文(reify / deftype,如 im.ttalk.agent.server.sse 的 SSE 正文); String、byte[]、File、InputStream、seq 这些 Ring 经 extend 支持的照旧交给 http-kit。不是 http-kit 发来的请求 (没有 :async-channel)原样放过。
  • 对端断开(http-kit 的 :on-close)⇒ 之后的写抛 IOException ⇒ SSE 流记 gone?、producer 据此收尾。 ⚠️ 已知局限(http-kit 2.8 实测,2026-11-10):客户端断开后 http-kit 的 send! 仍回 true、open? 仍为 true、 :on-close 不触发(关套接字后连写 40 次、每次 8 KB 都「成功」)。所以在 http-kit 上 SSE 发现不了断连:流一直写到 producer 自己结束——AG-UI 的 run 照常跑完、/stop 照常可用,但 :orphan-cancel-ms(最后一个订阅者走了就按停)不会生效。 要断连检测请用 Jetty(缺省)或 Undertow。
  • 不引 http-kit 的硬依赖:requiring-resolve 取 org.httpkit.server,用 http-kit 的人自己的 classpath 上有它。
http-kit 的垫片:让它也能发 **Ring SPEC 的流式正文**(`ring.core.protocols/StreamableResponseBody`)。

http-kit 2.8 的 adapter 不认识 SPEC 的流式正文(直接 500「not understandable」),惰性 seq / 管道 InputStream 正文
它也是攒满了才发(designs/server-module.md §1 的实测)。本中间件把这样的响应改走 http-kit 自己的 `as-channel`:
先发状态码与头,再在一条虚拟线程上 `write-body-to-stream` 到一个转发给 `send!` 的 OutputStream,每次 write 即推送。

```clojure
(require '[org.httpkit.server :as hk]
         '[im.ttalk.agent.server.adapter.http-kit :as hk-shim])
(hk/run-server (hk-shim/wrap-streaming handler) {:port 3000})
```

- 只改**直接实现** `StreamableResponseBody` 接口的正文(reify / deftype,如 `im.ttalk.agent.server.sse` 的 SSE 正文);
  String、byte[]、File、InputStream、seq 这些 Ring 经 `extend` 支持的照旧交给 http-kit。不是 http-kit 发来的请求
  (没有 `:async-channel`)原样放过。
- 对端断开(http-kit 的 `:on-close`)⇒ 之后的写抛 IOException ⇒ SSE 流记 `gone?`、producer 据此收尾。
  ⚠️ **已知局限**(http-kit 2.8 实测,2026-11-10):客户端断开后 http-kit 的 `send!` 仍回 true、`open?` 仍为 true、
  `:on-close` 不触发(关套接字后连写 40 次、每次 8 KB 都「成功」)。所以在 http-kit 上 **SSE 发现不了断连**:流一直写到
  producer 自己结束——AG-UI 的 run 照常跑完、`/stop` 照常可用,但 `:orphan-cancel-ms`(最后一个订阅者走了就按停)不会生效。
  要断连检测请用 Jetty(缺省)或 Undertow。
- 不引 http-kit 的硬依赖:`requiring-resolve` 取 `org.httpkit.server`,用 http-kit 的人自己的 classpath 上有它。
raw docstring

im.ttalk.agent.server.adapter.undertow

Undertow(luminus/ring-undertow-adapter)的垫片:让它也能发 Ring SPEC 的流式正文 (ring.core.protocols/StreamableResponseBody)。

luminus 的 adapter 不认识 SPEC 的流式正文(直接 500「Body class not supported」),InputStream / seq 正文又是 攒满了才发(designs/server-module.md §1 的实测)。它的正文分派是一个开放协议 ring.adapter.undertow.response/RespondBody——install! 给它补上 StreamableResponseBody 接口的实现: 离开 IO 线程、startBlocking、把 exchange 的 OutputStream 交给 write-body-to-stream(每次 flush 即推送)、收尾。

(require '[ring.adapter.undertow :refer [run-undertow]]
         '[im.ttalk.agent.server.adapter.undertow :as ut-shim])
(ut-shim/install!)                       ;; 进程里调一次(幂等)
(run-undertow handler {:port 3000})

只影响直接实现该接口的正文(reify / deftype);String、InputStream 等照旧走 adapter 自己的实现。 不引 Undertow 的硬依赖:requiring-resolve 取协议,用 Undertow 的人自己的 classpath 上有它。

Undertow(`luminus/ring-undertow-adapter`)的垫片:让它也能发 **Ring SPEC 的流式正文**
(`ring.core.protocols/StreamableResponseBody`)。

luminus 的 adapter 不认识 SPEC 的流式正文(直接 500「Body class not supported」),InputStream / seq 正文又是
攒满了才发(designs/server-module.md §1 的实测)。它的正文分派是一个**开放协议**
`ring.adapter.undertow.response/RespondBody`——`install!` 给它补上 `StreamableResponseBody` 接口的实现:
离开 IO 线程、`startBlocking`、把 exchange 的 OutputStream 交给 `write-body-to-stream`(每次 flush 即推送)、收尾。

```clojure
(require '[ring.adapter.undertow :refer [run-undertow]]
         '[im.ttalk.agent.server.adapter.undertow :as ut-shim])
(ut-shim/install!)                       ;; 进程里调一次(幂等)
(run-undertow handler {:port 3000})
```

只影响**直接实现**该接口的正文(reify / deftype);String、InputStream 等照旧走 adapter 自己的实现。
不引 Undertow 的硬依赖:`requiring-resolve` 取协议,用 Undertow 的人自己的 classpath 上有它。
raw docstring

im.ttalk.agent.server.agui

AG-UI 的 HTTP 端(C# Keelson.Agui.Server 的 AguiEndpoints;keel agui/http.ss + server-http.ss):CopilotKit runtime 顶替那一层 要的几条路,挂在任意前缀下。全是 Ring:routes 是 clj-net-server 的路由表(handler 直接给一个 Ring 处理器), SSE 是流式正文(StreamableResponseBody)—— 不认识 Jetty;clj-net-server 的宿主(Jetty 12)或任何 Ring 服务端都能挂。 run / 注册表 / 编解码 / 线程存储用上游 clj-agent-agui —— C# 的 Keelson.Agui(AguiRegistry、AguiRun、RunAgentInput …) 由它顶替,不重移植。

GET  {prefix}/info                         名录与能力位
POST {prefix}/agent/:id/run                纯 AG-UI 的 SSE
POST {prefix}/agent/:id/connect            只订阅、不起 run:重放在飞 / 刚跑完那一轮,跟到底;没有 ⇒ 200 空流
POST {prefix}/agent/:id/suggest            「猜你想问」,无状态(条目 :suggest? + :agent-spec 才有,不然 404)
POST {prefix}/agent/:id/stop/:threadId     按停(那条 RUN_ERROR code=STOPPED 在响应体里,⛔ 不发进流)
POST {prefix}                              单路由退路:{"method": …, "params": {…}, "body": …}
OPTIONS 任意                               预检(永远 204;真正的闸在真请求那一次)

—— 下面七条只在给了 :thread-store 时存在 ——
GET    {prefix}/threads                    {"threads":[…],"nextCursor":…}(查询串原样交给存储)
GET    {prefix}/threads/:id/messages       {"messages":[…]}
GET    {prefix}/threads/:id/events         {"events":[…]}
GET    {prefix}/threads/:id/state          {"state":…}
PATCH  {prefix}/threads/:id                那条线程记录(⛔ 不套信封)
POST   {prefix}/threads/:id/archive        {"threadId":…,"archived":true}
DELETE {prefix}/threads/:id                {"threadId":…,"deleted":true}(⚠️ 带 JSON 体)

认不出的路径:任何方法都 404 not_found(⛔ 不是 405)。CORS 缺省拒(见 im.ttalk.agent.server.agui.http)。 线程那几条:存储缺那个槽 ⇒ 501 not_implemented;没这条线程 ⇒ 404 thread_not_found;方法不对 ⇒ 405 带 Allow; 改动类的体不是对象 / 缺 agentId ⇒ 400;存储抛了 ⇒ 500 thread_store_error。…/threads/subscribe 与 …/threads/clear 照旧 404。

(def h (host/host reg {:version "1.0.0" :origin-allowed (ahttp/origin-allow "http://localhost:3000")}))
(def s (net/start (agui/routes "/api/copilotkit" h) {:port 3000}))   ; im.ttalk.agent.server.jetty(Jetty)
;; 或别的 Ring 服务端:(agui/handler "/api/copilotkit" h)

routes 返回 clj-net-server 的路由表,元数据里带停机钩子(停机时按停所有在飞的 run,原因 :shutdown)。

注册表条目(clj-agent-agui registry/registry)在 :id :runtime :description :suggest? :stop 之外还认: :agent-spec /suggest 要现建一次性 agent 的那份 create-agent 配置(无状态:临时 memory、不进注册表) :memory 这台的 ChatMemory(线程存储的适配器读 /threads/:id/messages) :server-tool-names 服务端工具名(前端同名声明 = 渲染 / 审批意图,见 im.ttalk.agent.server.agui.resume) :input-transform 入站插件:(fn [body] 输入),把 RunAgentInput 解析结果改一笔再起跑(如 clj-agent-agui 的 a2ui/input-transform) :mcp-proxy MCP Apps 的前端代理:(fn [proxied-request] result)。/run 的 forwardedProps.__proxiedMCPRequest 不是一次 run(前端 iframe 里的 MCP App 要调 server)——不起 agent,回一对 RUN_STARTED / RUN_FINISHED{result}(上游 CopilotKit 的做法;如 clj-agent-agui 的 mcp/proxy-request) 这两格也可以放在 host 选项里当缺省值,条目里给了就以条目为准。

自带 run 驱动的 agent:注册表条目只要 {:id "x" :external? true :description …}(⛔ 不塞占位 runtime);routes 的 host 选项收 :agents {"x" {:run ring-handler :stop ring-handler :connect-source source}}。run / stop 是普通 Ring 处理器 (Origin / CORS 已在外层处理,单路由退路也认它们;体超限由库先答 413,不交驱动);source 照 (thread-id after-seq deliver) 调,deliver 吃 AG-UI 事件 map 或已编帧字节,先写帧环再送本连接,交付终端事件后流自动收口。source 可回一个清理函数。

与 C# 的差别(详见 tasks/J-03.md):被按停的 run 在流里以 RUN_FINISHED result.status=cancelled(带 reason)收口 ——clj-agent-agui 的契约是「恰好一个终态」,C# 是什么都不再发;/connect 的 lastSeq 续读(IAguiThreadEventStore / 序号记录器)不做;对端断开要靠心跳的那一次写才发现(Ring 的流式正文 / Jetty 的阻塞输出不报告空闲时的断连)。

AG-UI 的 HTTP 端(C# Keelson.Agui.Server 的 AguiEndpoints;keel agui/http.ss + server-http.ss):CopilotKit runtime 顶替那一层
要的几条路,挂在任意前缀下。**全是 Ring**:`routes` 是 clj-net-server 的路由表(`handler` 直接给一个 Ring 处理器),
SSE 是流式正文(StreamableResponseBody)—— 不认识 Jetty;clj-net-server 的宿主(Jetty 12)或任何 Ring 服务端都能挂。
run / 注册表 / 编解码 / 线程存储用上游 clj-agent-agui —— C# 的 Keelson.Agui(AguiRegistry、AguiRun、RunAgentInput …)
由它顶替,不重移植。

```
GET  {prefix}/info                         名录与能力位
POST {prefix}/agent/:id/run                纯 AG-UI 的 SSE
POST {prefix}/agent/:id/connect            只订阅、不起 run:重放在飞 / 刚跑完那一轮,跟到底;没有 ⇒ 200 空流
POST {prefix}/agent/:id/suggest            「猜你想问」,无状态(条目 :suggest? + :agent-spec 才有,不然 404)
POST {prefix}/agent/:id/stop/:threadId     按停(那条 RUN_ERROR code=STOPPED 在响应体里,⛔ 不发进流)
POST {prefix}                              单路由退路:{"method": …, "params": {…}, "body": …}
OPTIONS 任意                               预检(永远 204;真正的闸在真请求那一次)

—— 下面七条只在给了 :thread-store 时存在 ——
GET    {prefix}/threads                    {"threads":[…],"nextCursor":…}(查询串原样交给存储)
GET    {prefix}/threads/:id/messages       {"messages":[…]}
GET    {prefix}/threads/:id/events         {"events":[…]}
GET    {prefix}/threads/:id/state          {"state":…}
PATCH  {prefix}/threads/:id                那条线程记录(⛔ 不套信封)
POST   {prefix}/threads/:id/archive        {"threadId":…,"archived":true}
DELETE {prefix}/threads/:id                {"threadId":…,"deleted":true}(⚠️ 带 JSON 体)
```
认不出的路径:任何方法都 404 `not_found`(⛔ 不是 405)。CORS 缺省**拒**(见 im.ttalk.agent.server.agui.http)。
线程那几条:存储缺那个槽 ⇒ 501 `not_implemented`;没这条线程 ⇒ 404 `thread_not_found`;方法不对 ⇒ 405 带 `Allow`;
改动类的体不是对象 / 缺 agentId ⇒ 400;存储抛了 ⇒ 500 `thread_store_error`。…/threads/subscribe 与 …/threads/clear 照旧 404。

```clojure
(def h (host/host reg {:version "1.0.0" :origin-allowed (ahttp/origin-allow "http://localhost:3000")}))
(def s (net/start (agui/routes "/api/copilotkit" h) {:port 3000}))   ; im.ttalk.agent.server.jetty(Jetty)
;; 或别的 Ring 服务端:(agui/handler "/api/copilotkit" h)
```
`routes` 返回 clj-net-server 的路由表,元数据里带停机钩子(停机时按停所有在飞的 run,原因 :shutdown)。

注册表条目(clj-agent-agui `registry/registry`)在 :id :runtime :description :suggest? :stop 之外还认:
  :agent-spec          `/suggest` 要现建一次性 agent 的那份 create-agent 配置(无状态:临时 memory、不进注册表)
  :memory              这台的 ChatMemory(线程存储的适配器读 /threads/:id/messages)
  :server-tool-names   服务端工具名(前端同名声明 = 渲染 / 审批意图,见 im.ttalk.agent.server.agui.resume)
  :input-transform     入站插件:(fn [body] 输入),把 RunAgentInput 解析结果改一笔再起跑(如 clj-agent-agui 的 `a2ui/input-transform`)
  :mcp-proxy           MCP Apps 的前端代理:(fn [proxied-request] result)。`/run` 的 `forwardedProps.__proxiedMCPRequest`
                       不是一次 run(前端 iframe 里的 MCP App 要调 server)——不起 agent,回一对 RUN_STARTED /
                       RUN_FINISHED{result}(上游 CopilotKit 的做法;如 clj-agent-agui 的 `mcp/proxy-request`)
这两格也可以放在 host 选项里当缺省值,条目里给了就以条目为准。

自带 run 驱动的 agent:注册表条目只要 `{:id "x" :external? true :description …}`(⛔ 不塞占位 runtime);`routes` 的
host 选项收 `:agents {"x" {:run ring-handler :stop ring-handler :connect-source source}}`。run / stop 是普通 Ring 处理器
(Origin / CORS 已在外层处理,单路由退路也认它们;体超限由库先答 413,不交驱动);source 照 `(thread-id after-seq deliver)` 调,deliver 吃 AG-UI 事件 map
或已编帧字节,先写帧环再送本连接,交付终端事件后流自动收口。source 可回一个清理函数。

**与 C# 的差别**(详见 tasks/J-03.md):被按停的 run 在流里以 `RUN_FINISHED result.status=cancelled`(带 reason)收口
——clj-agent-agui 的契约是「恰好一个终态」,C# 是什么都不再发;`/connect` 的 lastSeq 续读(IAguiThreadEventStore /
序号记录器)不做;对端断开要靠心跳的那一次写才发现(Ring 的流式正文 / Jetty 的阻塞输出不报告空闲时的断连)。
raw docstring

im.ttalk.agent.server.agui.host

成品服务端的运行期(C# AguiHost;keel server-http.ss N3):run 归服务端驱动,连接只是订阅者。

run 本身由 clj-agent-agui 的 runtime 跑(会话注册表、取消、HITL 都在那边);这里给每台经 /run 起的 run 挂一只泵 (一个 runtime 订阅):把中立事件编成 AG-UI 帧,进帧环(给 /connect,见 im.ttalk.agent.server.agui.replay)与直连队列 (给起 run 的那条连接),喂线程存储的 :record 钩子。⚠️ 断连 ≠ 按停(刷新也是断连,而刷新正是 /connect 要救的场景); 按停的正门是 /stop/:threadId。

选项(C# AguiRegistryOptions 里服务端用得上的那几格 + AguiServerOptions): :version 进 /info(缺省 "0.0.0") :origin-allowed Origin 白名单谓词;缺省 nil = 拒(带 Origin 的请求 403)。im.ttalk.agent.server.agui.http/origin-allow :credentials? 发 Access-Control-Allow-Credentials: true :allow-headers 宿主自己的请求头,并进预检的 Allow-Headers :a2ui? :open-generative-ui? /info 的两格布尔 :thread-store 线程存储:六槽 map 或 clj-agent-agui 的 ThreadStore(见 im.ttalk.agent.server.agui.thread-store);给了才有 /threads* :heartbeat-ms SSE 心跳(缺省 15000;0 = 关)。⚠️ 不是可有可无:空闲超时的代理会把不出声的流掐掉;JVM 上它还是发现断连的唯一途径 :heartbeat-frame (fn [] 字节);缺省注释帧 : clj-agent-alive。/run、/connect、/suggest 的流都用它 :replay-max-bytes 帧环一轮的字节上限(缺省 4 MiB;超了 ⇒ 这一轮 /connect 回空流) :replay-keep-ms 跑完之后留多久给迟到的 /connect(缺省 5 分钟) :replay-max-threads 帧环最多几条线程(缺省 256) :replay? 帧环攒不攒(缺省 true;false ⇒ /connect 对任何线程 200 空流,/run 照旧) :orphan-cancel-ms 最后一个订阅者走了之后过这么久没人来就按停(原因 :orphan;缺省 0 = 关) :max-body-bytes 请求体上限(缺省 16 MiB;超了 413) :now (fn [] ms),可注入 :info 其余交给 clj-agent-agui codec/run-info 的能力位(:multimodal :parallel-tools? :state? …) :input-transform 入站插件的缺省值(条目里给了就以条目为准;见 im.ttalk.agent.server.agui 的注册表条目键) :mcp-proxy MCP Apps 前端代理的缺省值(同上) :agents 按 agent 注入外部驱动的缺省值(routes 也能再并一份;键同下面的注册表条目键)

同线程上一轮还在飞时再来 /run(C# AguiRegistryOptions.ConcurrentRun)由那台 agent 的 runtime 决定: rt/runtime 的 :on-concurrent :reject(缺省,⇒ 409 thread_running)或 :supersede(旧的以 :superseded 取消)。

成品服务端的运行期(C# AguiHost;keel server-http.ss N3):**run 归服务端驱动,连接只是订阅者**。

run 本身由 clj-agent-agui 的 runtime 跑(会话注册表、取消、HITL 都在那边);这里给每台经 `/run` 起的 run 挂一只**泵**
(一个 runtime 订阅):把中立事件编成 AG-UI 帧,进帧环(给 `/connect`,见 im.ttalk.agent.server.agui.replay)与直连队列
(给起 run 的那条连接),喂线程存储的 :record 钩子。⚠️ 断连 ≠ 按停(刷新也是断连,而刷新正是 `/connect` 要救的场景);
按停的正门是 `/stop/:threadId`。

选项(C# AguiRegistryOptions 里服务端用得上的那几格 + AguiServerOptions):
  :version             进 /info(缺省 "0.0.0")
  :origin-allowed      Origin 白名单谓词;缺省 nil = **拒**(带 Origin 的请求 403)。im.ttalk.agent.server.agui.http/origin-allow
  :credentials?        发 Access-Control-Allow-Credentials: true
  :allow-headers       宿主自己的请求头,并进预检的 Allow-Headers
  :a2ui? :open-generative-ui?  /info 的两格布尔
  :thread-store        线程存储:六槽 map 或 clj-agent-agui 的 ThreadStore(见 im.ttalk.agent.server.agui.thread-store);给了才有 /threads*
  :heartbeat-ms        SSE 心跳(缺省 15000;0 = 关)。⚠️ 不是可有可无:空闲超时的代理会把不出声的流掐掉;JVM 上它还是发现断连的唯一途径
  :heartbeat-frame     (fn [] 字节);缺省注释帧 `: clj-agent-alive`。/run、/connect、/suggest 的流都用它
  :replay-max-bytes    帧环一轮的字节上限(缺省 4 MiB;超了 ⇒ 这一轮 /connect 回空流)
  :replay-keep-ms      跑完之后留多久给迟到的 /connect(缺省 5 分钟)
  :replay-max-threads  帧环最多几条线程(缺省 256)
  :replay?             帧环攒不攒(缺省 true;false ⇒ /connect 对任何线程 200 空流,/run 照旧)
  :orphan-cancel-ms    最后一个订阅者走了之后过这么久没人来就按停(原因 :orphan;缺省 0 = 关)
  :max-body-bytes      请求体上限(缺省 16 MiB;超了 413)
  :now                 (fn [] ms),可注入
  :info                其余交给 clj-agent-agui `codec/run-info` 的能力位(:multimodal :parallel-tools? :state? …)
  :input-transform     入站插件的缺省值(条目里给了就以条目为准;见 im.ttalk.agent.server.agui 的注册表条目键)
  :mcp-proxy           MCP Apps 前端代理的缺省值(同上)
  :agents              按 agent 注入外部驱动的缺省值(routes 也能再并一份;键同下面的注册表条目键)

同线程上一轮还在飞时再来 `/run`(C# AguiRegistryOptions.ConcurrentRun)由那台 agent 的 runtime 决定:
`rt/runtime` 的 `:on-concurrent :reject`(缺省,⇒ 409 thread_running)或 `:supersede`(旧的以 :superseded 取消)。
raw docstring

im.ttalk.agent.server.agui.http

routes 每条路外面那一层(C# AguiHttp,A-16 公开):预检、Origin 闸、CORS 回声,外加 JSON / 错误出口 —— 全是 Ring (请求 map 进、响应 map 出)。宿主在自己补的路上用它,⛔ 不用照抄 CORS:

(conj (agui/routes "/agui" host)
      ["POST" "/agui/agent/mine/stop/:threadId"
       (fn [req] (ahttp/guard req host (fn [req] (ahttp/json-response 200 {:stopped true}))))])

与 clj-net-server 的访问守卫(im.ttalk.agent.server.guard)是两层:守卫在路由之前(Host → 预检豁免 → token); 这里在路由之后,只管浏览器那一侧的 Origin / CORS。预检到这里永远 204(403 的预检在浏览器里只报一句 CORS error)。

CORS 缺省拒:带 Origin 的请求一律 403(host 选项 :origin-allowed;用 origin-allow 列白名单,开发时 origin-any)。

`routes` 每条路外面那一层(C# AguiHttp,A-16 公开):预检、Origin 闸、CORS 回声,外加 JSON / 错误出口 —— 全是 Ring
(请求 map 进、响应 map 出)。宿主在自己补的路上用它,⛔ 不用照抄 CORS:

```clojure
(conj (agui/routes "/agui" host)
      ["POST" "/agui/agent/mine/stop/:threadId"
       (fn [req] (ahttp/guard req host (fn [req] (ahttp/json-response 200 {:stopped true}))))])
```

与 clj-net-server 的访问守卫(im.ttalk.agent.server.guard)是两层:守卫在路由之前(Host → 预检豁免 → token);
这里在路由之后,只管浏览器那一侧的 Origin / CORS。预检到这里永远 204(403 的预检在浏览器里只报一句 CORS error)。

CORS 缺省**拒**:带 Origin 的请求一律 403(host 选项 :origin-allowed;用 `origin-allow` 列白名单,开发时 `origin-any`)。
raw docstring

im.ttalk.agent.server.agui.input

AG-UI 的 run 输入(C# Keelson.Agui 的 RunAgentInput.Decode):只做形状校验,码与文案同 C#;解析成什么交给 clj-agent-agui 的 codec/parse-run-input(历史取服务端权威、只取最后一条 user 消息 …)。

错误是 ex-info::im.ttalk.agent.server/code = C# 的资源键(agui-input/…),:agui/wire-code = 线上的种类码 (bad_json / bad_input),路由据此回 400 {"error": wire-code, "message": …}。

AG-UI 的 run 输入(C# Keelson.Agui 的 RunAgentInput.Decode):只做**形状**校验,码与文案同 C#;解析成什么交给
clj-agent-agui 的 `codec/parse-run-input`(历史取服务端权威、只取最后一条 user 消息 …)。

错误是 ex-info:`:im.ttalk.agent.server/code` = C# 的资源键(`agui-input/…`),`:agui/wire-code` = 线上的种类码
(`bad_json` / `bad_input`),路由据此回 400 `{"error": wire-code, "message": …}`。
raw docstring

im.ttalk.agent.server.agui.replay

/connect 的帧环(C# Keelson.Agui 的 AguiReplay;keel agui/replay.ss,designs/agui-connect.md N2):按线程一只有界缓冲, 只装这一轮从 RUN_STARTED 起编好的帧(字节)。是传输缓冲不是会话存储:跑完留 keep-ms 给迟到的 /connect, 新一轮开始就覆盖。

为什么不直接用 clj-agent-agui 的事件存储(:event-store / subscribe :since):那是按条数有界、跨 run 连续的会话环, 没有 C# 契约要的三样 —— 按字节封顶且超了整轮作废(truncated ⇒ /connect 回空流,⛔ 不发半截)、跑完后按时长保留、 线程数 LRU 驱逐。那三样是小东西,就在这里补上。

超 max-bytes ⇒ 这一轮标 :truncated、清空、不再攒。线程数超 max-threads ⇒ 驱逐没在跑的里最久没碰的(全在跑时宁可多一条)。 时间由调用方传进来(毫秒)。线程安全(一把锁)。:enabled? false ⇒ 空转:不建条目、不进帧、open-cursor 恒 nil (⇒ /connect 对任何线程回 200 空流)。

`/connect` 的帧环(C# Keelson.Agui 的 AguiReplay;keel agui/replay.ss,designs/agui-connect.md N2):按线程一只有界缓冲,
只装**这一轮**从 RUN_STARTED 起编好的帧(字节)。是**传输**缓冲不是会话存储:跑完留 keep-ms 给迟到的 `/connect`,
新一轮开始就覆盖。

为什么不直接用 clj-agent-agui 的事件存储(`:event-store` / `subscribe :since`):那是按**条数**有界、跨 run 连续的会话环,
没有 C# 契约要的三样 —— 按**字节**封顶且超了整轮作废(truncated ⇒ `/connect` 回空流,⛔ 不发半截)、跑完后按时长保留、
线程数 LRU 驱逐。那三样是小东西,就在这里补上。

超 max-bytes ⇒ 这一轮标 :truncated、清空、不再攒。线程数超 max-threads ⇒ 驱逐**没在跑的**里最久没碰的(全在跑时宁可多一条)。
时间由调用方传进来(毫秒)。线程安全(一把锁)。`:enabled? false` ⇒ 空转:不建条目、不进帧、`open-cursor` 恒 nil
(⇒ `/connect` 对任何线程回 200 空流)。
raw docstring

im.ttalk.agent.server.agui.resume

/run 起跑前的那一段:新 run 还是续跑(interrupt 协议的 resume[]、前端工具的结果、审批答复)。 逻辑最初出自 clj-agent 的 examples/copilotkit/http_kit_routes.clj(handle-run* / resume-plan / resume-decision …);2026-11-10 起 那份示例改为调用本模块,这里是唯一的一份。 文案走本模块的 i18n。C# 那边这一段在 ChatAguiAgent 里(Keelson.Agui),协议层只管路由。

start! 返回 runtime 的结果(rt/start-run! / rt/resume-run!:{:status :started :run-id :since}、:busy、 :awaiting-resume、:not-paused),或 {:status :resume-error :error [status code extra]}(在起 run 之前判掉的坏答复)。

`/run` 起跑前的那一段:新 run 还是续跑(interrupt 协议的 `resume[]`、前端工具的结果、审批答复)。
逻辑最初出自 clj-agent 的 examples/copilotkit/http_kit_routes.clj(handle-run* / resume-plan / resume-decision …);2026-11-10 起
那份示例改为调用本模块,这里是唯一的一份。
文案走本模块的 i18n。C# 那边这一段在 ChatAguiAgent 里(Keelson.Agui),协议层只管路由。

`start!` 返回 runtime 的结果(`rt/start-run!` / `rt/resume-run!`:`{:status :started :run-id :since}`、`:busy`、
`:awaiting-resume`、`:not-paused`),或 `{:status :resume-error :error [status code extra]}`(在起 run 之前判掉的坏答复)。
raw docstring

im.ttalk.agent.server.agui.thread-store

…/threads* 那七条路背后的存储(C# Keelson.Agui 的 IAguiThreadStore / AguiThreadStore 委托版)。

契约是 C# 的六个可选槽,在这里写成一个 fn 的 map —— 给了哪个键就亮哪个槽,没给的那几条路回 501:

:list (fn [query] …) GET /threads。query = 查询串原样 {"agentId" "echo" "limit" "2"}(值全是串,同名取第一个)。 回线程记录的序列 ⇒ 补成 {threads, nextCursor: null};回带 :threads 的 map ⇒ 原样出(缺 nextCursor 补 null) :messages (fn [thread-id] …) GET /threads/:id/messages ⇒ {messages};nil = 没这条线程(404) :events (fn [thread-id] …) GET /threads/:id/events ⇒ {events};nil = 没这条线程 :state (fn [thread-id] …) GET /threads/:id/state。回 nil = 没这条线程(404);回 {:state v} = 有(v 可以是 nil ⇒ {"state":null}) :update (fn [thread-id agent-id updates] …) PATCH /threads/:id(改名 / 取消归档)与 POST /threads/:id/archive(updates = {:archived true})。 updates 已去掉 agentId / userId。回改过的那条线程记录(PATCH ⇒ 裸的出);nil = 没这条线程 :delete (fn [thread-id agent-id] …) DELETE /threads/:id。false = 没这条线程

外加一个可选钩子(C# IAguiThreadRecorder): :record (fn [agent-id input agui-event]) 每台 run 的每条 AG-UI 事件(RUN_STARTED 是第一条,input 是那台的 RunAgentInput)。 在泵上同步调:要快;抛了吞掉(⛔ 不打断 run)。/suggest 不喂

抛异常 ⇒ 500 thread_store_error,带着异常的话(⛔ 不吞)。

接 clj-agent-agui:它的 thread-store/ThreadStore 只管元数据(三个槽:list / update / delete),消息在 ChatMemory、事件在 runtime 的事件存储 —— from-clj-agent 把这三样拼成上面的六槽 map(host 收到一个 ThreadStore 时自动套上)。

`…/threads*` 那七条路背后的存储(C# Keelson.Agui 的 IAguiThreadStore / AguiThreadStore 委托版)。

**契约是 C# 的六个可选槽**,在这里写成一个 fn 的 map —— 给了哪个键就亮哪个槽,没给的那几条路回 501:

  :list     (fn [query] …)           GET /threads。query = 查询串原样 {"agentId" "echo" "limit" "2"}(值全是串,同名取第一个)。
                                     回线程记录的序列 ⇒ 补成 {threads, nextCursor: null};回带 :threads 的 map ⇒ 原样出(缺 nextCursor 补 null)
  :messages (fn [thread-id] …)       GET /threads/:id/messages ⇒ {messages};nil = 没这条线程(404)
  :events   (fn [thread-id] …)       GET /threads/:id/events ⇒ {events};nil = 没这条线程
  :state    (fn [thread-id] …)       GET /threads/:id/state。回 nil = 没这条线程(404);回 {:state v} = 有(v 可以是 nil ⇒ {"state":null})
  :update   (fn [thread-id agent-id updates] …)  PATCH /threads/:id(改名 / 取消归档)与 POST /threads/:id/archive(updates = {:archived true})。
                                     updates 已去掉 agentId / userId。回改过的那条线程记录(PATCH ⇒ **裸的**出);nil = 没这条线程
  :delete   (fn [thread-id agent-id] …)  DELETE /threads/:id。false = 没这条线程

外加一个可选钩子(C# IAguiThreadRecorder):
  :record   (fn [agent-id input agui-event])  每台 run 的每条 AG-UI 事件(RUN_STARTED 是第一条,input 是那台的 RunAgentInput)。
                                     在泵上同步调:要快;抛了吞掉(⛔ 不打断 run)。`/suggest` 不喂

抛异常 ⇒ 500 `thread_store_error`,带着异常的话(⛔ 不吞)。

**接 clj-agent-agui**:它的 `thread-store/ThreadStore` 只管元数据(三个槽:list / update / delete),消息在 ChatMemory、事件在
runtime 的事件存储 —— `from-clj-agent` 把这三样拼成上面的六槽 map(host 收到一个 ThreadStore 时自动套上)。
raw docstring

im.ttalk.agent.server.guard

本机访问守卫:token + Host 头(C# AccessGuard,N-05;语义同锦书 AccessGuard ≙ jinshu2 (jinshu httpd guard))。纯判定 + 中间件。

wrap-access-guard 是 Ring 中间件(不认识具体服务端)。选项(map;经 im.ttalk.agent.server.jetty 的 :guard 挂到最前面,或自己包): :token 要求的 token;nil / 空串 = 不鉴权(只做 Host 校验,若开了的话) :loopback-host-only? Host 头只认回环(localhost、127.0.0.0/8、::1,带不带端口、IPv6 带方括号都认)—— 挡 DNS rebinding。 ⚠️ 要经局域网 IP / 域名访问的部署别开 :protected (fn [path] bool),哪些路径要 token(参数是解码后的路径,如 "/rpc")。缺省全部 :preflight-exempt (fn [req] bool),免 token 的请求(在 token 之前、Host 之后判)。缺省:方法是 OPTIONS(CORS 预检) :query-name query 里带 token 的参数名(EventSource 不能自定义头)。缺省 "t";显式 nil = 不认这条带法 :header-name 带 token 的请求头名。缺省 "X-Token";显式 nil = 不认 :accept-bearer? 认 Authorization: Bearer <token>(scheme 大小写不敏感)。缺省 true

判定次序(evaluate):① :loopback-host-only? ⇒ Host 不是回环 ⇒ 403(所有请求,含静态资源与预检 —— DNS rebinding 的请求 带着对的 token 也拒);② :preflight-exempt ⇒ 放行;③ token 非空且 :protected ⇒ 三种带法里有一种对得上才放行,否则 401。

本机访问守卫:token + Host 头(C# AccessGuard,N-05;语义同锦书 AccessGuard ≙ jinshu2 (jinshu httpd guard))。纯判定 + 中间件。

`wrap-access-guard` 是 Ring 中间件(不认识具体服务端)。选项(map;经 im.ttalk.agent.server.jetty 的 `:guard` 挂到最前面,或自己包):
  :token               要求的 token;nil / 空串 = 不鉴权(只做 Host 校验,若开了的话)
  :loopback-host-only? Host 头只认回环(localhost、127.0.0.0/8、::1,带不带端口、IPv6 带方括号都认)—— 挡 DNS rebinding。
                       ⚠️ 要经局域网 IP / 域名访问的部署别开
  :protected           (fn [path] bool),哪些路径要 token(参数是解码后的路径,如 "/rpc")。缺省全部
  :preflight-exempt    (fn [req] bool),免 token 的请求(在 token 之前、Host 之后判)。缺省:方法是 OPTIONS(CORS 预检)
  :query-name          query 里带 token 的参数名(EventSource 不能自定义头)。缺省 "t";显式 nil = 不认这条带法
  :header-name         带 token 的请求头名。缺省 "X-Token";显式 nil = 不认
  :accept-bearer?      认 `Authorization: Bearer <token>`(scheme 大小写不敏感)。缺省 true

判定次序(`evaluate`):① :loopback-host-only? ⇒ Host 不是回环 ⇒ 403(**所有**请求,含静态资源与预检 —— DNS rebinding 的请求
带着对的 token 也拒);② :preflight-exempt ⇒ 放行;③ token 非空且 :protected ⇒ 三种带法里有一种对得上才放行,否则 401。
raw docstring

im.ttalk.agent.server.http

Ring 请求 / 响应的小工具(C# 里 ASP.NET 的 HttpContext 那一侧)。只认 Ring 规范(请求 map / 响应 map),不认识任何具体的 服务端 —— 同一个处理器挂在 Jetty(im.ttalk.agent.server.jetty)或别的 Ring 服务端(http-kit …)上都行。

请求就是 Ring 的请求 map(:request-method :uri :query-string :headers(小写名):body)。本模块另认几个键: :path-params 路由参数 {:id "…"}(已解码;im.ttalk.agent.server.routing 填) :im.ttalk.agent.server/body-limit 请求体上限(wrap-body-limit 填;read-body 守它) :im.ttalk.agent.server/stopping 服务端停机时完成的 CompletableFuture(宿主填;没有 = 宿主不报停机) :im.ttalk.agent.server/open-streams 宿主的「开着的长连接」表(SSE / WebSocket 登记进去,停机时逐个中止)

响应就是 Ring 的响应 map;response / json-response / text-response 造它。

Ring 请求 / 响应的小工具(C# 里 ASP.NET 的 HttpContext 那一侧)。**只认 Ring 规范**(请求 map / 响应 map),不认识任何具体的
服务端 —— 同一个处理器挂在 Jetty(im.ttalk.agent.server.jetty)或别的 Ring 服务端(http-kit …)上都行。

请求就是 Ring 的请求 map(:request-method :uri :query-string :headers(小写名):body)。本模块另认几个键:
  :path-params                  路由参数 {:id "…"}(已解码;im.ttalk.agent.server.routing 填)
  :im.ttalk.agent.server/body-limit   请求体上限(`wrap-body-limit` 填;`read-body` 守它)
  :im.ttalk.agent.server/stopping     服务端停机时完成的 CompletableFuture(宿主填;没有 = 宿主不报停机)
  :im.ttalk.agent.server/open-streams 宿主的「开着的长连接」表(SSE / WebSocket 登记进去,停机时逐个中止)

响应就是 Ring 的响应 map;`response` / `json-response` / `text-response` 造它。
raw docstring

im.ttalk.agent.server.mcp

MCP 的 Streamable HTTP 端点——Ring 处理器(designs/server-module.md §4)。协议在 clj-agent-mcp: im.ttalk.agent.mcp.server/handle-message(纯函数,一条进一条出)与 check-headers(HTTP 头与信封一致性); 这里只做 HTTP 那一层。转正自 examples/mcp/server_example.clj 的 http-kit 接线。

(require '[im.ttalk.agent.mcp.server :as srv]
         '[im.ttalk.agent.server.mcp :as mcp-http])
(def s (srv/server {:name "demo" :version "1.0" :tools [...]}))
(mcp-http/handler s {:origin-allowed nil})        ;; 一个 Ring 处理器,交给任何 Ring 服务端
(mcp-http/routes "/mcp" s opts)                   ;; 或 im.ttalk.agent.server.routing 的路由表
  • POST:读体(受 :max-body-bytes,超了 413)→ 解析 JSON(坏 ⇒ 400 -32700;JSON 数组 ⇒ 400 -32600,批量自 2025-06-18 起已从协议删除)→ check-headers(头与信封不一致 ⇒ 400 -32020)→ handle-message → 有响应 ⇒ JSON, 校验类错误(-32602 -32020 -32021 -32022)400、其余 200(客户端的双时代探测看状态码);通知 ⇒ 202 空体(协议禁止给 通知回响应)。handle-message 意外抛出 ⇒ 500 -32603(不把异常文本吐给客户端)。
  • GET / DELETE / 其他 ⇒ 405 带 Allow: POST, OPTIONS:本端点不开服务端推送流、也没有会话(规范允许)。
  • OPTIONS ⇒ 预检 204。
  • Origin 缺省拒:带 Origin 且不在 :origin-allowed 里 ⇒ 403(规范要求服务端校验 Origin 防 DNS rebinding); 不带 Origin(非浏览器客户端)放行。白名单用 im.ttalk.agent.server.agui.http/origin-allow。
  • 只回 JSON、不回 SSE:一条请求一条响应,没有要推的中间消息(规范允许服务端选 JSON)。

选项::origin-allowed(Origin 谓词;缺省 nil = 拒所有带 Origin 的):credentials?(回 Allow-Credentials) :allow-headers(预检额外放行的请求头):max-body-bytes(缺省 4 MiB)。

MCP 的 Streamable HTTP 端点——**Ring 处理器**(designs/server-module.md §4)。协议在 clj-agent-mcp:
`im.ttalk.agent.mcp.server/handle-message`(纯函数,一条进一条出)与 `check-headers`(HTTP 头与信封一致性);
这里只做 HTTP 那一层。转正自 examples/mcp/server_example.clj 的 http-kit 接线。

```clojure
(require '[im.ttalk.agent.mcp.server :as srv]
         '[im.ttalk.agent.server.mcp :as mcp-http])
(def s (srv/server {:name "demo" :version "1.0" :tools [...]}))
(mcp-http/handler s {:origin-allowed nil})        ;; 一个 Ring 处理器,交给任何 Ring 服务端
(mcp-http/routes "/mcp" s opts)                   ;; 或 im.ttalk.agent.server.routing 的路由表
```

- `POST`:读体(受 `:max-body-bytes`,超了 413)→ 解析 JSON(坏 ⇒ 400 `-32700`;JSON 数组 ⇒ 400 `-32600`,批量自
  2025-06-18 起已从协议删除)→ `check-headers`(头与信封不一致 ⇒ 400 `-32020`)→ `handle-message` → 有响应 ⇒ JSON,
  校验类错误(`-32602 -32020 -32021 -32022`)400、其余 200(客户端的双时代探测看状态码);通知 ⇒ 202 空体(协议禁止给
  通知回响应)。`handle-message` 意外抛出 ⇒ 500 `-32603`(不把异常文本吐给客户端)。
- `GET` / `DELETE` / 其他 ⇒ 405 带 `Allow: POST, OPTIONS`:本端点不开服务端推送流、也没有会话(规范允许)。
- `OPTIONS` ⇒ 预检 204。
- **Origin 缺省拒**:带 Origin 且不在 `:origin-allowed` 里 ⇒ 403(规范要求服务端校验 Origin 防 DNS rebinding);
  不带 Origin(非浏览器客户端)放行。白名单用 `im.ttalk.agent.server.agui.http/origin-allow`。
- 只回 JSON、不回 SSE:一条请求一条响应,没有要推的中间消息(规范允许服务端选 JSON)。

选项:`:origin-allowed`(Origin 谓词;缺省 nil = 拒所有带 Origin 的)`:credentials?`(回 Allow-Credentials)
`:allow-headers`(预检额外放行的请求头)`:max-body-bytes`(缺省 4 MiB)。
raw docstring

im.ttalk.agent.server.routing

路由(C# 里 ASP.NET 的 MapGet / MapPost / Map / MapGroup 那一层,够 Agui.Server 用的大小):路由表 ⇒ 一个 Ring 处理器。

为什么不用 reitit:它对「字面量段 vs 参数段」的冲突要么装配期报错、要么退成按登记顺序的线性匹配,而 C# 的契约是按具体度选 (宿主后登记的 agent/mine/stop/:id 要胜过先登记的 agent/:id/stop/:id),还要「路径中、方法不中 ⇒ 405 + Allow, 兜底段(任何方法)在时 ⇒ 不是 405」。这一百来行比把 reitit 的冲突解析与缺省处理器拧成这个样子更直接。

一条路 = [方法 模式 处理器]: 方法 :any | "GET" | #{"GET" "HEAD"}(大写) 模式 "/agent/:id/run":字面量段、:名 参数段(匹配一段,值已解码)、末尾 *名 兜底段(匹配零或多段,值是余下的 路径,段间以 / 连接)。字面量段大小写不敏感(ASP.NET 同);末尾的 / 不计("/a/" 与 "/a" 是同一条)。 处理器 Ring 处理器 (fn [req] 响应),req 带 :path-params。返回 nil ⇒ 200 空正文(ASP.NET 同:处理器什么都没写)。

选路(ASP.NET 的口径):路径匹配的那些里挑最具体的 —— 逐段比,字面量 < 参数 < 兜底,同等时先登记的胜;方法不中的跳过。 路径中了而方法一条都不中 ⇒ 405 + Allow;路径都不中 ⇒ 404(正文为空)。 产出的处理器两种元数都有(Ring 异步的 3 元:respond / raise 在同一线程上调)。

路由(C# 里 ASP.NET 的 MapGet / MapPost / Map / MapGroup 那一层,够 Agui.Server 用的大小):路由表 ⇒ 一个 **Ring 处理器**。

为什么不用 reitit:它对「字面量段 vs 参数段」的冲突要么装配期报错、要么退成按登记顺序的线性匹配,而 C# 的契约是**按具体度**选
(宿主后登记的 `agent/mine/stop/:id` 要胜过先登记的 `agent/:id/stop/:id`),还要「路径中、方法不中 ⇒ 405 + Allow,
兜底段(任何方法)在时 ⇒ 不是 405」。这一百来行比把 reitit 的冲突解析与缺省处理器拧成这个样子更直接。

一条路 = `[方法 模式 处理器]`:
  方法   :any | "GET" | #{"GET" "HEAD"}(大写)
  模式   "/agent/:id/run":字面量段、`:名` 参数段(匹配一段,值已解码)、末尾 `*名` 兜底段(匹配零或多段,值是余下的
         路径,段间以 / 连接)。字面量段大小写不敏感(ASP.NET 同);末尾的 / 不计("/a/" 与 "/a" 是同一条)。
  处理器 Ring 处理器 (fn [req] 响应),req 带 :path-params。返回 nil ⇒ 200 空正文(ASP.NET 同:处理器什么都没写)。

选路(ASP.NET 的口径):路径匹配的那些里挑**最具体**的 —— 逐段比,字面量 < 参数 < 兜底,同等时先登记的胜;方法不中的跳过。
路径中了而方法一条都不中 ⇒ 405 + `Allow`;路径都不中 ⇒ 404(正文为空)。
产出的处理器两种元数都有(Ring 异步的 3 元:respond / raise 在同一线程上调)。
raw docstring

im.ttalk.agent.server.sse

SSE 响应(C# SseResponse / SseStream),Ring 的写法:处理器返回 (response req opts producer) —— 一个 Ring 响应 map, 正文是 ring.core.protocols/StreamableResponseBody;服务端写正文时调 (producer s),producer 在那条线程上往流里写帧 (每帧冲一次),返回即收尾。帧由 im.ttalk.net.sse(event-frame / comment-frame)或调用方自己拼好。

流(producer 收到的 s):写入串行化(心跳与业务写入互斥);写失败(对端走了)不抛,记为 gone?,后续写入直接丢弃 —— 调用方据此退订。aborted 是一个 CompletableFuture:对端断开(在写的那一刻发现)、服务端停机(宿主经请求里的 :im.ttalk.agent.server/open-streams 通知)或 close! 时完成。

⚠️ 对端断开只在写的时候发现:Jetty 的阻塞式 servlet 输出在不写的时候不报告连接已断(Ring 的正文协议也没有这个通道), 所以闲着的流靠心跳(:heartbeat-ms)发现断连。C# 的 RequestAborted 还盯着套接字的读端。

SSE 响应(C# SseResponse / SseStream),**Ring 的写法**:处理器返回 `(response req opts producer)` —— 一个 Ring 响应 map,
正文是 `ring.core.protocols/StreamableResponseBody`;服务端写正文时调 `(producer s)`,producer 在那条线程上往流里写帧
(每帧冲一次),返回即收尾。帧由 im.ttalk.net.sse(`event-frame` / `comment-frame`)或调用方自己拼好。

流(producer 收到的 `s`):写入串行化(心跳与业务写入互斥);写失败(对端走了)不抛,记为 `gone?`,后续写入直接丢弃 ——
调用方据此退订。`aborted` 是一个 CompletableFuture:对端断开(在写的那一刻发现)、服务端停机(宿主经请求里的
:im.ttalk.agent.server/open-streams 通知)或 `close!` 时完成。

⚠️ 对端断开**只在写的时候**发现:Jetty 的阻塞式 servlet 输出在不写的时候不报告连接已断(Ring 的正文协议也没有这个通道),
所以闲着的流靠心跳(:heartbeat-ms)发现断连。C# 的 RequestAborted 还盯着套接字的读端。
raw docstring

im.ttalk.agent.server.strings

clj-agent-server 的文案与错误。

(msg k & args) ⇒ {:code "区/事件" :text "渲染后的话"}({0} {1} … 占位);(error k args data) ⇒ ex-info, ex-data 带 :im.ttalk.agent.server/code(= :code)。码与文案移植自 Keelson(clj-net-server / clj-agui-server 由 C# resx 生成的中文文案包,提交 5899776c);线上的错误种类码(bad_json、thread_not_found …)不在这里,它们是 AG-UI 的线协议。

clj-agent-server 的文案与错误。

`(msg k & args)` ⇒ `{:code "区/事件" :text "渲染后的话"}`(`{0}` `{1}` … 占位);`(error k args data)` ⇒ ex-info,
ex-data 带 `:im.ttalk.agent.server/code`(= :code)。码与文案移植自 Keelson(`clj-net-server` / `clj-agui-server` 由 C# resx
生成的中文文案包,提交 5899776c);线上的错误种类码(`bad_json`、`thread_not_found` …)不在这里,它们是 AG-UI 的线协议。
raw 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