声明式审批策略、会话审批缓存、审批者链、拒绝熔断(designs/runtime-borrow.md §5 §6;
借 Keelson Tools/ApprovalPolicy.cs RulePolicy.cs ApprovalCache.cs ToolHintPolicy.cs
与 Advisors/ChainApprover.cs DenialBreaker.cs Approvers.cs,它们又借 Codex execpolicy /
guardian)。
全是纯函数与数据,产出现有挂点能直接吃的东西:
| 出口 | 给谁 | 场景 |
|---|---|---|
gate | react :tool-gate / simple-agent :approval | HITL:要问就暂停等人 |
approval-filter | chat-client :filters(:tool 链) | 同步审批(CLI / 审批服务 / 模型审阅者) |
->approve-fn | 旧的 flt/approval-filter | 兼容 |
决策:{:decision :allow|:prompt|:forbid :reason 字符串? :allow-session? bool},
多条取最严(forbid > prompt > allow)。
策略:(fn [req] -> 决策),req = {:name :args :sensitive? :inline? :hints :tool-call}。
审批者:(fn [req] -> {:approved? bool :reason? :remember? :timed-out?} | nil),
nil = 不表态(交给链上的下一位)。:remember :session = 「本会话都批准」。
声明式审批策略、会话审批缓存、审批者链、拒绝熔断(designs/runtime-borrow.md §5 §6;
借 Keelson `Tools/ApprovalPolicy.cs` `RulePolicy.cs` `ApprovalCache.cs` `ToolHintPolicy.cs`
与 `Advisors/ChainApprover.cs` `DenialBreaker.cs` `Approvers.cs`,它们又借 Codex execpolicy /
guardian)。
全是**纯函数与数据**,产出现有挂点能直接吃的东西:
| 出口 | 给谁 | 场景 |
|---|---|---|
| `gate` | react `:tool-gate` / simple-agent `:approval` | HITL:要问就**暂停**等人 |
| `approval-filter` | chat-client `:filters`(`:tool` 链) | 同步审批(CLI / 审批服务 / 模型审阅者) |
| `->approve-fn` | 旧的 `flt/approval-filter` | 兼容 |
**决策**:`{:decision :allow|:prompt|:forbid :reason 字符串? :allow-session? bool}`,
多条取最严(forbid > prompt > allow)。
**策略**:`(fn [req] -> 决策)`,req = `{:name :args :sensitive? :inline? :hints :tool-call}`。
**审批者**:`(fn [req] -> {:approved? bool :reason? :remember? :timed-out?} | nil)`,
nil = 不表态(交给链上的下一位)。`:remember :session` = 「本会话都批准」。模型审阅者(designs/runtime-borrow.md §7;借 Keelson Advisors/ModelApprover.cs,
它又借 Codex guardian-reviewer)。
把工具名、策略理由、参数与最近的对话(都有 token 预算)交给一个(通常更小的) 模型,要它回严格 JSON:
{"risk_level":"low|medium|high|critical",
"user_authorization":"unknown|low|medium|high",
"outcome":"allow|deny", "rationale":"..."}
outcome = allow → 批,deny → 拒(rationale 作理由回给主模型)。失败即拒:超时、
模型抛错、回的不是合格 JSON → 拒;:abstain-on-failure? true 时改为不表态(nil),
交给审批链的下一环(典型:审阅不了再问人)。
产出的是 im.ttalk.agent.approval 的审批者,放进 chain-approver /
approval-filter :approver 即可。core 零依赖,故自带一个最小 JSON 读取器。
模型审阅者(designs/runtime-borrow.md §7;借 Keelson `Advisors/ModelApprover.cs`,
它又借 Codex guardian-reviewer)。
把工具名、策略理由、参数与最近的对话(**都有 token 预算**)交给一个(通常更小的)
模型,要它回严格 JSON:
{"risk_level":"low|medium|high|critical",
"user_authorization":"unknown|low|medium|high",
"outcome":"allow|deny", "rationale":"..."}
`outcome` = allow → 批,deny → 拒(rationale 作理由回给主模型)。**失败即拒**:超时、
模型抛错、回的不是合格 JSON → 拒;`:abstain-on-failure? true` 时改为不表态(nil),
交给审批链的下一环(典型:审阅不了再问人)。
产出的是 `im.ttalk.agent.approval` 的**审批者**,放进 `chain-approver` /
`approval-filter :approver` 即可。core 零依赖,故自带一个最小 JSON 读取器。Agent 独立回调系统 — 与 chat-client filter 完全解耦。
9 个回调钩子(对标 beamai_agent_callbacks): :on-turn-start (fn [metadata]) 新 turn 开始 :on-turn-end (fn [metadata]) turn 正常完成 :on-turn-error (fn [error metadata]) turn 出错 :on-llm-call (fn [messages metadata]) 每次 LLM 调用前(观察用) :on-llm-result (fn [response metadata]) 每次 LLM 返回后(观察用) :on-tool-call (fn [tool-name args]) tool 调用前;返回 {:interrupt reason} 触发中断 :on-tool-result (fn [tool-name result]) tool 执行后(观察用)
注意:tool-name 两处都是字符串(非 keyword)——拿 keyword 去 = 比较
会永不相等,于是 :on-tool-call 的中断判断静默失效(曾真的踩过)。
:on-interrupt (fn [interrupt-info metadata]) 进入中断状态时
:on-resume (fn [interrupt-info metadata]) 从中断状态恢复时
设计原则:
与 chat-client filter 的区别:
Agent 独立回调系统 — 与 chat-client filter 完全解耦。
9 个回调钩子(对标 beamai_agent_callbacks):
:on-turn-start (fn [metadata]) 新 turn 开始
:on-turn-end (fn [metadata]) turn 正常完成
:on-turn-error (fn [error metadata]) turn 出错
:on-llm-call (fn [messages metadata]) 每次 LLM 调用前(观察用)
:on-llm-result (fn [response metadata]) 每次 LLM 返回后(观察用)
:on-tool-call (fn [tool-name args]) tool 调用前;返回 {:interrupt reason} 触发中断
:on-tool-result (fn [tool-name result]) tool 执行后(观察用)
注意:`tool-name` 两处都是**字符串**(非 keyword)——拿 keyword 去 `=` 比较
会永不相等,于是 `:on-tool-call` 的中断判断静默失效(曾真的踩过)。
:on-interrupt (fn [interrupt-info metadata]) 进入中断状态时
:on-resume (fn [interrupt-info metadata]) 从中断状态恢复时
设计原则:
- 大部分回调仅观察,不影响主流程
- :on-tool-call 是唯一可影响流程的回调(返回 {:interrupt reason} 触发中断)
- 回调抛异常时静默忽略(确保不中断主流程)
- 与 chat-client filter 完全解耦:回调不走 filter 链,直接在关键节点触发
与 chat-client filter 的区别:
- chat-client filter 洋葱式 around 拦截,可改写请求/响应,适合 memory/retry/cache
- callback 轻量事件通知,适合日志/监控/审计/告警/中断控制SimpleAgent 公共构建逻辑
提供 chat-client 构建与结果处理等共享功能。 对话历史由 ChatClient 的 ChatMemory store 自管(Memory Filter 模式), Agent 自身只持 conversation-id + 轻量 state。
SimpleAgent 公共构建逻辑 提供 chat-client 构建与结果处理等共享功能。 对话历史由 ChatClient 的 ChatMemory store 自管(Memory Filter 模式), Agent 自身只持 conversation-id + 轻量 state。
回合生命周期事件 + 指标挂点(designs/runtime-borrow.md §11;借 Keelson
AgentEvents.cs / AgentMetrics.cs,它们又借 Codex 的 TurnStarted / TokenCount /
codex.turn.ttft)。
一个封闭的小集合:UI / 遥测从一处拿到回合、轮次、LLM、工具始末与用量,不必
各自从零散回调里拼。出口是 callbacks 的 :on-event (fn [event])——react 在关键节点
直接触发(react/emit-event!),simple-agent 的 :callbacks 原样透传。
:type | 键 |
|---|---|
:turn/started | :resumed? |
:iteration/started | :index |
:llm/finished | :index :elapsed-ms :ttft-ms(仅流式) :usage :finish-reason |
:usage | :index :round :total(本轮 / 本回合累计) |
:tool/started | :index :call-id :name :args |
:tool/finished | :index :call-id :name :status :error-class? :elapsed-ms |
:turn/finished | :status :elapsed-ms :usage :stopped-by? :error? |
每个事件都带 :type :ts(毫秒),以及回调元数据里的 :conversation-id / :run-id。
:tool/finished 的 :status ∈ :ok :error :rejected :replied(gate 拒绝 / 答复即结果
也有始末)。⚠️ 并发引擎下工具事件可能来自不同线程;出口抛的异常被吞掉(UI 的问题
不打断回合)。零依赖:要接 OpenTelemetry / Micrometer 自己在 :on-event 里转。
回合生命周期事件 + 指标挂点(designs/runtime-borrow.md §11;借 Keelson `AgentEvents.cs` / `AgentMetrics.cs`,它们又借 Codex 的 TurnStarted / TokenCount / `codex.turn.ttft`)。 **一个封闭的小集合**:UI / 遥测从一处拿到回合、轮次、LLM、工具始末与用量,不必 各自从零散回调里拼。出口是 callbacks 的 `:on-event (fn [event])`——react 在关键节点 直接触发(`react/emit-event!`),simple-agent 的 `:callbacks` 原样透传。 | `:type` | 键 | |---|---| | `:turn/started` | `:resumed?` | | `:iteration/started` | `:index` | | `:llm/finished` | `:index :elapsed-ms :ttft-ms(仅流式) :usage :finish-reason` | | `:usage` | `:index :round :total`(本轮 / 本回合累计) | | `:tool/started` | `:index :call-id :name :args` | | `:tool/finished` | `:index :call-id :name :status :error-class? :elapsed-ms` | | `:turn/finished` | `:status :elapsed-ms :usage :stopped-by? :error?` | 每个事件都带 `:type` `:ts`(毫秒),以及回调元数据里的 `:conversation-id` / `:run-id`。 `:tool/finished` 的 `:status` ∈ `:ok :error :rejected :replied`(gate 拒绝 / 答复即结果 也有始末)。⚠️ 并发引擎下工具事件可能来自不同线程;出口抛的异常被吞掉(UI 的问题 不打断回合)。零依赖:要接 OpenTelemetry / Micrometer 自己在 `:on-event` 里转。
摘要式上下文压缩(designs/runtime-borrow.md §1;借 Keelson
Advisors/CompactionAdvisor.cs / CompactionBudget.cs,判据又照 Reactor / Codex)。
长会话迟早撞窗。本 filter 按预算比例触发:
挂法::chat filter,注册在 memory filter 之后(看到的是展开后的完整历史)。
与 ChatMemory 的关系(缺省不写回):store 仍是完整历史的唯一真相——AG-UI 的
消息快照、timeline、审计都读它。压缩只改这一次发给模型的视图;为了不每轮重摘,
filter 按会话记一份摘要缓存 {:prefix-n :prefix-hash :summary}:历史前 n 条没变
就直接换成缓存的摘要(零 LLM 调用)。再超阈值时在视图上继续压(旧摘要进新摘要的
素材,逐次合并)。要「有界存储」就给 :write-back-store:摘要后用压缩结果改写 store
(mem-clear + mem-add,消息 id 保留;前端看到的历史随之变短,代价自负)。
两个可替换挂点(2026-11-08 用户拍板:缺省不改写、不落盘,但逻辑要能整个换掉,
同 Keelson Agui.Bitcask 那样由外部库提供实现):
:store:CompactionStore 协议,存每会话的可持久部分 {:cache :failures}
(摘要缓存 + 摘要器连续失败数)。缺省 in-memory-store(LRU,进程停即丢)。
外部实现可落盘,重启后不必重摘。缓存对不上(前缀 hash 变了)只会触发重摘,不会出错。:write-back:(fn [conversation-id compacted original]),摘要成功后调用,决定怎么
处置历史(改写 / 归档原文再改写 / 只记审计…)。缺省 nil = 不写回;
:write-back-store 是它的语法糖(= (store-write-back store))。摘要式上下文压缩(designs/runtime-borrow.md §1;借 Keelson
`Advisors/CompactionAdvisor.cs` / `CompactionBudget.cs`,判据又照 Reactor / Codex)。
长会话迟早撞窗。本 filter 按预算比例触发:
1. **微压缩**:更早的工具结果正文换成占位符、保留的截到上限(不调模型);
2. 仍超 → 在**安全切点**(不劈开 assistant 调用与它的结果)把前缀换成一条**摘要**;
3. 没有摘要器 / 无可摘前缀 / 摘要失败 → **丢最老**(按安全段丢);
4. provider 报**上下文超长** → 强压后重发一次。
**挂法**:`:chat` filter,注册在 memory filter **之后**(看到的是展开后的完整历史)。
**与 ChatMemory 的关系(缺省不写回)**:store 仍是完整历史的唯一真相——AG-UI 的
消息快照、timeline、审计都读它。压缩只改**这一次发给模型的视图**;为了不每轮重摘,
filter 按会话记一份**摘要缓存** `{:prefix-n :prefix-hash :summary}`:历史前 n 条没变
就直接换成缓存的摘要(零 LLM 调用)。再超阈值时在视图上继续压(旧摘要进新摘要的
素材,逐次合并)。要「有界存储」就给 `:write-back-store`:摘要后用压缩结果改写 store
(`mem-clear` + `mem-add`,消息 id 保留;前端看到的历史随之变短,代价自负)。
**两个可替换挂点**(2026-11-08 用户拍板:缺省不改写、不落盘,但逻辑要能整个换掉,
同 Keelson `Agui.Bitcask` 那样由外部库提供实现):
- `:store`:`CompactionStore` 协议,存每会话的**可持久部分** `{:cache :failures}`
(摘要缓存 + 摘要器连续失败数)。缺省 `in-memory-store`(LRU,进程停即丢)。
外部实现可落盘,重启后不必重摘。缓存对不上(前缀 hash 变了)只会触发重摘,不会出错。
- `:write-back`:`(fn [conversation-id compacted original])`,摘要成功后调用,决定怎么
处置历史(改写 / 归档原文再改写 / 只记审计…)。缺省 nil = 不写回;
`:write-back-store` 是它的语法糖(= `(store-write-back store)`)。死循环提醒(designs/runtime-borrow.md §4;借 Keelson Advisors/DeadLoopAdvisor.cs,
它又借 keel dead-loop-filter)。
模型在两三个调用之间打转(读同一个文件、查同一个状态),每轮都成功,所以失败保护
(filter.tool-guard)管不到。本 filter 记最近 :window 轮的调用指纹,最近 :recent
轮里不超过 :distinct 种、且这几轮没有真进展 → 给下一轮挂一条临时提醒
(react 的 :notes 挂点:只附在那一次请求末尾,不进 ChatMemory)。
⛔ 只提醒、不阻断:误阻断会把正在做的事拦腰截断,漏检只是多烧几轮 token (上面还有 max-iterations 与 tool-guard)。提醒过就清空窗口。
挂法::iteration + :turn(后者在新 turn 开始时清零状态)。状态按 conversation-id
放在 filter 闭包里——只是提醒,不进暂停快照,丢了无害。
死循环提醒(designs/runtime-borrow.md §4;借 Keelson `Advisors/DeadLoopAdvisor.cs`, 它又借 keel `dead-loop-filter`)。 模型在两三个调用之间打转(读同一个文件、查同一个状态),每轮都**成功**,所以失败保护 (`filter.tool-guard`)管不到。本 filter 记最近 `:window` 轮的调用指纹,最近 `:recent` 轮里不超过 `:distinct` 种、且这几轮没有真进展 → 给**下一轮**挂一条临时提醒 (react 的 `:notes` 挂点:只附在那一次请求末尾,不进 ChatMemory)。 ⛔ **只提醒、不阻断**:误阻断会把正在做的事拦腰截断,漏检只是多烧几轮 token (上面还有 max-iterations 与 tool-guard)。提醒过就清空窗口。 挂法:`:iteration` + `:turn`(后者在新 turn 开始时清零状态)。状态按 conversation-id 放在 filter 闭包里——只是提醒,不进暂停快照,丢了无害。
Memory Filter - 按 conversation-id 读写历史
以 around-chat filter 形态挂进 chat-client 的 filter 链:
conversation-id 从 ChatRequest 的 :context(扁平 map)读取;为空时整体 no-op (保留一次性/无记忆调用)。store 是 filter 的私有闭包,chat-client 对其无感知。
用法: (build-chat-client {:chat-model cm :tools tools :filters [(memory-filter store)]})
Memory Filter - 按 conversation-id 读写历史
以 around-chat filter 形态挂进 chat-client 的 filter 链:
- req 段:把本次入参(delta)存入 store,再用完整历史替换发给 LLM 的 messages
- resp 段:把 LLM 的 assistant 回复(可能含 tool-calls)存入 store
conversation-id 从 ChatRequest 的 :context(扁平 map)读取;为空时整体 no-op
(保留一次性/无记忆调用)。store 是 filter 的私有闭包,chat-client 对其无感知。
用法:
(build-chat-client {:chat-model cm
:tools tools
:filters [(memory-filter store)]})ChatMemory - 按 conversation-id 管理中立消息历史
这是 Memory Filter 的后端存储。只存中立消息(见 core/llm/message)。 替代了旧 clj-agent-memory 模块中庞大的快照/时间线/多后端体系, 这里只保留对话历史所需的极小子集。
协议:
实现:
ChatMemory - 按 conversation-id 管理中立消息历史 这是 Memory Filter 的后端存储。只存中立消息(见 core/llm/message)。 替代了旧 clj-agent-memory 模块中庞大的快照/时间线/多后端体系, 这里只保留对话历史所需的极小子集。 协议: - mem-get [store conv-id] -> [中立消息 ...] - mem-add [store conv-id msgs] 追加 - mem-clear [store conv-id] 实现: - (in-memory-store) atom 后端 - (windowed store opts) 在 get 时按窗口裁剪(非破坏,底层仍存完整历史)
持久化 ChatMemory - SQLite 后端
把中立消息(见 core/llm/message)按 conversation-id 持久化到 SQLite, 进程重启后仍可恢复历史(配合 create-agent 的 :conversation-id 实现按用户恢复)。
消息以 EDN 字符串存储(中立消息是纯 Clojure 数据,EDN 可无损往返)。
表结构: chat_messages(id INTEGER PK AUTOINCREMENT, conversation_id TEXT, content TEXT)
用法: (require '[im.ttalk.agent.memory.sqlite :as sqlite]) (def store (sqlite/sqlite-store "agent.db")) (create-agent {:provider p :memory store :conversation-id "user-123"})
持久化 ChatMemory - SQLite 后端
把中立消息(见 core/llm/message)按 conversation-id 持久化到 SQLite,
进程重启后仍可恢复历史(配合 create-agent 的 :conversation-id 实现按用户恢复)。
消息以 EDN 字符串存储(中立消息是纯 Clojure 数据,EDN 可无损往返)。
表结构:
chat_messages(id INTEGER PK AUTOINCREMENT, conversation_id TEXT, content TEXT)
用法:
(require '[im.ttalk.agent.memory.sqlite :as sqlite])
(def store (sqlite/sqlite-store "agent.db"))
(create-agent {:provider p :memory store :conversation-id "user-123"})HITL 暂停态持久化 - PauseStore(设计见 docs/agent-loop-concurrency-design.md §11)
Agent 暂停(审批 / :env-retry 环境失败)后,暂停快照按 conversation-id 持久化;进程重启后用同一 conversation-id + 同一 store 重建 agent, resume 透明恢复(client/paused?、client/resume 自动回落到 store)。
快照是纯 EDN 数据(loop-state/pending-tool 本就不含函数;chat-client/gate/ callbacks 由代码侧在 resume 时重新提供)。tool-context 存档前剥离 不可 EDN 序列化的 value(如 :chat-client),恢复时由调用方按需注回。
loop-state 与 pending-tool 不走剥离,必须自身可 EDN 往返——尤其
不能放 record:record 打印成 #ns.Foo{...},edn/read-string 没有对应
reader tag 会抛,于是 sqlite-pause-store 的 pause-load 整份快照读不
回来。而 in-memory-pause-store 直接存对象、毫发无伤——「单进程测试全绿,
重启后 resume 崩」正是这个组合的形态。护栏在 pause-test 的
loop-state-edn-roundtrip-test(审批与 :env-retry 两个 phase 各钉一次
pr-str → read-string → resume)。构造点见 react.clj 的 env-pause
与 run-tool-loop 的审批暂停分支。
边界:只持久化「暂停点」这个一致快照;批次执行中途的进程崩溃恢复 (durable execution)不在此范围。
用法: (def ps (sqlite-pause-store "agent.db")) ;; 或 (in-memory-pause-store) (create-agent {:provider p :memory (sqlite/sqlite-store "agent.db") :pause-store ps :conversation-id "order-42" ...}) ;; 暂停自动落库;重启后同配置重建 agent → (resume agent "approved")
HITL 暂停态持久化 - PauseStore(设计见 docs/agent-loop-concurrency-design.md §11)
Agent 暂停(审批 / :env-retry 环境失败)后,暂停快照按 conversation-id
持久化;进程重启后用同一 conversation-id + 同一 store 重建 agent,
resume 透明恢复(client/paused?、client/resume 自动回落到 store)。
快照是纯 EDN 数据(loop-state/pending-tool 本就不含函数;chat-client/gate/
callbacks 由代码侧在 resume 时重新提供)。tool-context 存档前剥离
不可 EDN 序列化的 value(如 :chat-client),恢复时由调用方按需注回。
**loop-state 与 pending-tool 不走剥离,必须自身可 EDN 往返**——尤其
**不能放 record**:record 打印成 `#ns.Foo{...}`,`edn/read-string` 没有对应
reader tag 会**抛**,于是 `sqlite-pause-store` 的 `pause-load` 整份快照读不
回来。而 `in-memory-pause-store` 直接存对象、毫发无伤——「单进程测试全绿,
重启后 resume 崩」正是这个组合的形态。护栏在 `pause-test` 的
`loop-state-edn-roundtrip-test`(审批与 :env-retry 两个 phase 各钉一次
`pr-str` → `read-string` → resume)。构造点见 react.clj 的 `env-pause`
与 run-tool-loop 的审批暂停分支。
边界:只持久化「暂停点」这个一致快照;批次执行中途的进程崩溃恢复
(durable execution)不在此范围。
用法:
(def ps (sqlite-pause-store "agent.db")) ;; 或 (in-memory-pause-store)
(create-agent {:provider p :memory (sqlite/sqlite-store "agent.db")
:pause-store ps :conversation-id "order-42" ...})
;; 暂停自动落库;重启后同配置重建 agent → (resume agent "approved")工具调用循环(从 chat-client 下沉而来)
chat-client 只提供原语 invoke-chat / invoke-tool;'驱动 LLM↔工具直到文本或暂停' 这套策略(含 max-iterations / gate 暂停 / resume / 悬空 tool_use 自愈)属编排层, 放在 simpleagent。每轮只向 invoke-chat 传 delta,由 chat-client 的 memory filter 按 conversation-id 拼出完整历史。
store 显式传入(chat-client 不再持有 memory):用于 heal 与临时会话清理; 与 chat-client 上挂载的 memory-filter 必须是同一个 store 实例。
callbacks 独立于 chat-client filter::on-llm-call/:on-llm-result/:on-tool-result 在循环关键节点直接触发,不走 filter 链。gate 评估结果缓存,确保每个工具调用 恰好触发一次观察回调(不重复)。
工具调用循环(从 chat-client 下沉而来) chat-client 只提供原语 invoke-chat / invoke-tool;'驱动 LLM↔工具直到文本或暂停' 这套策略(含 max-iterations / gate 暂停 / resume / 悬空 tool_use 自愈)属编排层, 放在 simpleagent。每轮只向 invoke-chat 传 delta,由 chat-client 的 memory filter 按 conversation-id 拼出完整历史。 store 显式传入(chat-client 不再持有 memory):用于 heal 与临时会话清理; 与 chat-client 上挂载的 memory-filter 必须是同一个 store 实例。 callbacks 独立于 chat-client filter::on-llm-call/:on-llm-result/:on-tool-result 在循环关键节点直接触发,不走 filter 链。gate 评估结果缓存,确保每个工具调用 恰好触发一次观察回调(不重复)。
SimpleAgent - 统一 Agent(合并原 kernel-agent / process-agent)
对话历史由 ChatClient 的 ChatMemory store 按 conversation-id 自管(Memory Filter)。 Agent 只持 conversation-id + 轻量控制状态。
Callback 体系(独立于 chat-client filter): 通过 :callbacks map 注册 9 个回调,用于监控和控制 agent 执行过程: :on-turn-start (fn [metadata]) 新 turn 开始 :on-turn-end (fn [metadata]) turn 正常完成 :on-turn-error (fn [error metadata]) turn 出错 :on-llm-call (fn [messages metadata]) 每次 LLM 调用前(在 react 层触发) :on-llm-result (fn [response metadata]) 每次 LLM 返回后(在 react 层触发) :on-tool-call (fn [tool-name args]) tool 调用前;返回 {:interrupt reason} 触发中断 :on-tool-result (fn [tool-name result]) tool 执行后(在 react 层触发) :on-interrupt (fn [interrupt-info metadata]) 进入中断状态时 :on-resume (fn [interrupt-info metadata]) 从中断状态恢复时
pause/resume 为可选能力:配置 callbacks :on-tool-call 即启用。
线程安全:单个 agent 实例不可被多线程并发 chat/resume。每个 agent 应绑定 单一对话线程;并发请按会话各建一个 agent(共享底层持久 store + 各自 :conversation-id 即可隔离)。
使用示例:
;; 简单同步 (def a (create-agent {:provider p :model "glm-4.7" :tools [#'get-weather]})) (chat a "北京天气?") ;; => {:status :completed :text "..." :tool-calls-made [...]}
;; 带 callbacks 的可观测 agent (def a (create-agent {:provider p :tools [#'my-tool] :callbacks {:on-turn-start (fn [m] (println "Turn" (:turn-count m))) :on-tool-result (fn [n r] (println n "=>" r)) :on-tool-call (fn [n a] (when (risky? n) {:interrupt :need-approval}))}}))
;; 工具审批:on-tool-call 返回 {:interrupt reason} 触发暂停 (def a (create-agent {:provider p :tools [#'delete-file] :callbacks {:on-tool-call (fn [n _] (when (= :delete-file n) {:interrupt "需要审批"})) :on-interrupt (fn [info _] (println "等待审批" (:reason info)))}})) (let [r (chat a "删除 /tmp/x")] (when (= :paused (:status r)) (resume a "approved")))
SimpleAgent - 统一 Agent(合并原 kernel-agent / process-agent)
对话历史由 ChatClient 的 ChatMemory store 按 conversation-id 自管(Memory Filter)。
Agent 只持 conversation-id + 轻量控制状态。
**Callback 体系**(独立于 chat-client filter):
通过 :callbacks map 注册 9 个回调,用于监控和控制 agent 执行过程:
:on-turn-start (fn [metadata]) 新 turn 开始
:on-turn-end (fn [metadata]) turn 正常完成
:on-turn-error (fn [error metadata]) turn 出错
:on-llm-call (fn [messages metadata]) 每次 LLM 调用前(在 react 层触发)
:on-llm-result (fn [response metadata]) 每次 LLM 返回后(在 react 层触发)
:on-tool-call (fn [tool-name args]) tool 调用前;返回 {:interrupt reason} 触发中断
:on-tool-result (fn [tool-name result]) tool 执行后(在 react 层触发)
:on-interrupt (fn [interrupt-info metadata]) 进入中断状态时
:on-resume (fn [interrupt-info metadata]) 从中断状态恢复时
pause/resume 为可选能力:**配置 callbacks :on-tool-call 即启用**。
线程安全:单个 agent 实例不可被多线程并发 chat/resume。每个 agent 应绑定
单一对话线程;并发请按会话各建一个 agent(共享底层持久 store + 各自
:conversation-id 即可隔离)。
使用示例:
;; 简单同步
(def a (create-agent {:provider p :model "glm-4.7" :tools [#'get-weather]}))
(chat a "北京天气?") ;; => {:status :completed :text "..." :tool-calls-made [...]}
;; 带 callbacks 的可观测 agent
(def a (create-agent {:provider p :tools [#'my-tool]
:callbacks {:on-turn-start (fn [m] (println "Turn" (:turn-count m)))
:on-tool-result (fn [n r] (println n "=>" r))
:on-tool-call (fn [n a] (when (risky? n) {:interrupt :need-approval}))}}))
;; 工具审批:on-tool-call 返回 {:interrupt reason} 触发暂停
(def a (create-agent {:provider p :tools [#'delete-file]
:callbacks {:on-tool-call (fn [n _] (when (= :delete-file n) {:interrupt "需要审批"}))
:on-interrupt (fn [info _] (println "等待审批" (:reason info)))}}))
(let [r (chat a "删除 /tmp/x")]
(when (= :paused (:status r))
(resume a "approved")))子 Agent 委派工具构建器(对标 beamai_agent_delegate)
提供三类入口:
delegate-tool:spawn→await→drop,同步拿结果。fanout-tool:tasks 列表,并发启动多个子 agent,汇总。management-tools:spawn/list/result/kill/restart,供 LLM 跨轮自管。每个函数返回内联工具 map(含 :handler),可直接放入 :tools 列表。
三类入口的 config 都认一个可选的 :observer——协议无关的观察挂点,
原样透进 manager/spawn! 的 spec(契约见 manager 的 ns docstring)。
AG-UI 那侧用它把委派期间的 token / 工具调用归属到一条 lane
(docs/subagent-event-attribution-design.md);不传就是今天的行为,逐字不变。
使用示例: (def research-tool (delegate-tool {:name "deep_research" :subagent-fn (fn [_ _] {:provider p :model "gpt-4o"}) :timeout 120000})) (create-agent {:tools [#'my-tool research-tool] ...})
子 Agent 委派工具构建器(对标 beamai_agent_delegate)
提供三类入口:
1. **同步单委派** `delegate-tool`:spawn→await→drop,同步拿结果。
2. **同步并发 fan-out** `fanout-tool`:tasks 列表,并发启动多个子 agent,汇总。
3. **异步管理工具集** `management-tools`:spawn/list/result/kill/restart,供 LLM 跨轮自管。
每个函数返回**内联工具 map**(含 :handler),可直接放入 :tools 列表。
三类入口的 config 都认一个可选的 `:observer`——**协议无关的观察挂点**,
原样透进 `manager/spawn!` 的 spec(契约见 `manager` 的 ns docstring)。
AG-UI 那侧用它把委派期间的 token / 工具调用归属到一条 lane
(docs/subagent-event-attribution-design.md);不传就是今天的行为,逐字不变。
使用示例:
(def research-tool
(delegate-tool {:name "deep_research"
:subagent-fn (fn [_ _] {:provider p :model "gpt-4o"})
:timeout 120000}))
(create-agent {:tools [#'my-tool research-tool] ...})子 Agent 管理器 — 受管异步注册表(对标 beamai_subagent_manager)
维护一个全局 atom 注册表,每个子 agent 在虚拟线程上运行 (j.u.c.Future 语义,kill! 走 future-cancel 中断)。
状态机::running → :done | :failed | :killed
使用模式:
同步委派(spawn→await→drop): (let [{:ok id} (spawn! spec)] (await! id 60000) (drop! id))
异步管理(LLM 跨轮自管): (let [{:ok id} (spawn! spec)] ;; 稍后查询 (result id) ;; 或 kill (kill! id))
spec 格式: {:subagent-config map — 传给 client/create-agent 的配置 :prompt string — 子 agent 的用户输入 :result-fn fn? — (fn [chat-result] -> string),默认取 :text :owner any? — 归属标识,用于 list-agents 过滤 :observer fn? — 观察者工厂,见下 :subagent-name string — 给观察者看的标签(缺省 "subagent") :task string — 同上,缺省取 :prompt :parent-tool-call-id string — 同上,发起本次委派的 tool-call id}
观察者(:observer)——协议无关的观察挂点
(fn [{:keys [id name task owner attempt parent-tool-call-id]}] -> {:decorate :chat-opts :start! :settle!})
| 键 | 类型 | 说明 |
|---|---|---|
:decorate | (fn [agent] agent') | 给子 agent 挂采集(如事件发射)。抛异常 → 用原 agent |
:chat-opts | map | 并进子 agent 的 chat opts(如 :on-token) |
:start! | (fn []) | 建 agent 之前 |
:settle! | (fn [outcome]) | 在 finally 里,故 kill / 超时 / 崩溃三条路径都覆盖得到 |
三条契约:
restart! 算新的一次,
:attempt 递增)——fan-out N 路 = N 个观察者实例,各自独立;agui.subagent)负责把这四个钩子
翻译成事件——client 模块不依赖 agui,反向依赖也不存在。子 Agent 管理器 — 受管异步注册表(对标 beamai_subagent_manager)
维护一个全局 atom 注册表,每个子 agent 在虚拟线程上运行
(j.u.c.Future 语义,kill! 走 future-cancel 中断)。
状态机::running → :done | :failed | :killed
使用模式:
1. 同步委派(spawn→await→drop):
(let [{:ok id} (spawn! spec)]
(await! id 60000)
(drop! id))
2. 异步管理(LLM 跨轮自管):
(let [{:ok id} (spawn! spec)]
;; 稍后查询
(result id)
;; 或 kill
(kill! id))
spec 格式:
{:subagent-config map — 传给 client/create-agent 的配置
:prompt string — 子 agent 的用户输入
:result-fn fn? — (fn [chat-result] -> string),默认取 :text
:owner any? — 归属标识,用于 list-agents 过滤
:observer fn? — 观察者工厂,见下
:subagent-name string — 给观察者看的标签(缺省 "subagent")
:task string — 同上,缺省取 :prompt
:parent-tool-call-id string — 同上,发起本次委派的 tool-call id}
**观察者(`:observer`)——协议无关的观察挂点**
`(fn [{:keys [id name task owner attempt parent-tool-call-id]}]
-> {:decorate :chat-opts :start! :settle!})`
| 键 | 类型 | 说明 |
|---|---|---|
| `:decorate` | `(fn [agent] agent')` | 给子 agent 挂采集(如事件发射)。抛异常 → 用原 agent |
| `:chat-opts` | map | 并进子 agent 的 `chat` opts(如 `:on-token`) |
| `:start!` | `(fn [])` | 建 agent 之前 |
| `:settle!` | `(fn [outcome])` | **在 `finally` 里**,故 kill / 超时 / 崩溃三条路径都覆盖得到 |
三条契约:
1. **工厂在 worker 线程上调用,每次 spawn 一次**(`restart!` 算新的一次,
`:attempt` 递增)——fan-out N 路 = N 个观察者实例,各自独立;
2. **四个钩子全可选、各自吞异常**:观察者永远不许改变被观察者的行为;
3. 本 ns **不认识任何协议**。AG-UI 那侧(`agui.subagent`)负责把这四个钩子
翻译成事件——client 模块不依赖 agui,反向依赖也不存在。子代理异步工具组(designs/runtime-borrow.md §14;借 Keelson Subagent/SubagentTools.cs)。
delegate/management-tools 有 spawn/list/result/kill/restart,但缺三样:
wait(阻塞等一组子 agent,带超时——不然模型只能空转轮询 subagent_result)、
send(给已完成的子 agent 续一句话,带着它自己的历史接着干)、上限
(同时运行的数量、嵌套深度——不然一个模型能把 fan-out 套成指数)。
五个内联工具:spawn_agent / send_agent / wait_agent / list_agents /
interrupt_agent,基于 subagent/manager。
:tool-context 种子 :subagent/depth 显式传递(设计原则 §3:
跨 ChatClient 边界只走显式通道);子代理异步工具组(designs/runtime-borrow.md §14;借 Keelson `Subagent/SubagentTools.cs`)。 `delegate/management-tools` 有 spawn/list/result/kill/restart,但缺三样: **wait**(阻塞等一组子 agent,带超时——不然模型只能空转轮询 `subagent_result`)、 **send**(给已完成的子 agent 续一句话,带着它自己的历史接着干)、**上限** (同时运行的数量、嵌套深度——不然一个模型能把 fan-out 套成指数)。 五个内联工具:`spawn_agent` / `send_agent` / `wait_agent` / `list_agents` / `interrupt_agent`,基于 `subagent/manager`。 - 深度经子 agent 的 `:tool-context` 种子 `:subagent/depth` **显式**传递(设计原则 §3: 跨 ChatClient 边界只走显式通道); - send = 同一 conversation-id + 同一 memory store 再起一次运行,历史自然续上; - 对模型暴露的 id 稳定不变(send 之后内部 spawn id 换了,表里记着映射)。
对话时间线与多分支(设计见 docs/agent-loop-concurrency-design.md §12)
Agent 的持久状态 = 对话历史(ChatMemory 中的 append-only 日志), 日志本身就是 timeline;分支 = fork-as-new-conversation——前缀复制到 新 conversation-id + 一条血缘记录。所有既有组件(memory filter / PauseStore / react 循环)按 conversation-id 工作,换分支即换 conv-id 建 agent,无组件感知"树"。
一致性不变量:合法的 fork/rollback 点只有 turn 边界与暂停点 (屏障才是一致快照点)。fork 暂停中的会话(全量前缀)自动连带复制 PauseStore 快照——两个分支可各自 resume 不同决策(HITL 决策分支)。
用法: (def deps {:memory mem :pause-store ps :branch (in-memory-branch-store)}) (fork! deps "main" {}) ;; 全量分支(含暂停快照) (fork! deps "main" {:at 4 :as "exp"}) ;; 前 4 条消息处开分支 (rollback! deps "main" 4) ;; 破坏性截断("重新生成") (ancestry deps "exp") ;; 血缘回溯 (prune! deps "exp") ;; 删分支(有子分支拒绝)
对话时间线与多分支(设计见 docs/agent-loop-concurrency-design.md §12)
Agent 的持久状态 = 对话历史(ChatMemory 中的 append-only 日志),
日志本身就是 timeline;分支 = fork-as-new-conversation——前缀复制到
新 conversation-id + 一条血缘记录。所有既有组件(memory filter /
PauseStore / react 循环)按 conversation-id 工作,换分支即换 conv-id
建 agent,无组件感知"树"。
一致性不变量:**合法的 fork/rollback 点只有 turn 边界与暂停点**
(屏障才是一致快照点)。fork 暂停中的会话(全量前缀)自动连带复制
PauseStore 快照——两个分支可各自 resume 不同决策(HITL 决策分支)。
用法:
(def deps {:memory mem :pause-store ps :branch (in-memory-branch-store)})
(fork! deps "main" {}) ;; 全量分支(含暂停快照)
(fork! deps "main" {:at 4 :as "exp"}) ;; 前 4 条消息处开分支
(rollback! deps "main" 4) ;; 破坏性截断("重新生成")
(ancestry deps "exp") ;; 血缘回溯
(prune! deps "exp") ;; 删分支(有子分支拒绝)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 |