Liking cljdoc? Tell your friends :D

im.ttalk.agent.approval

声明式审批策略、会话审批缓存、审批者链、拒绝熔断(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)。

全是纯函数与数据,产出现有挂点能直接吃的东西:

出口给谁场景
gatereact :tool-gate / simple-agent :approvalHITL:要问就暂停等人
approval-filterchat-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` = 「本会话都批准」。
raw docstring

im.ttalk.agent.approval.model

模型审阅者(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 读取器。
raw docstring

im.ttalk.agent.callbacks

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 轻量事件通知,适合日志/监控/审计/告警/中断控制
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       轻量事件通知,适合日志/监控/审计/告警/中断控制
raw docstring

im.ttalk.agent.common

SimpleAgent 公共构建逻辑

提供 chat-client 构建与结果处理等共享功能。 对话历史由 ChatClient 的 ChatMemory store 自管(Memory Filter 模式), Agent 自身只持 conversation-id + 轻量 state。

SimpleAgent 公共构建逻辑

提供 chat-client 构建与结果处理等共享功能。
对话历史由 ChatClient 的 ChatMemory store 自管(Memory Filter 模式),
Agent 自身只持 conversation-id + 轻量 state。
raw docstring

im.ttalk.agent.events

回合生命周期事件 + 指标挂点(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` 里转。
raw docstring

im.ttalk.agent.filter.compaction

摘要式上下文压缩(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 §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)`)。
raw docstring

im.ttalk.agent.filter.dead-loop

死循环提醒(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 闭包里——只是提醒,不进暂停快照,丢了无害。
raw docstring

im.ttalk.agent.filter.memory

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)]})

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)]})
raw docstring

im.ttalk.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 - 按 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 时按窗口裁剪(非破坏,底层仍存完整历史)
raw docstring

im.ttalk.agent.memory.sqlite

持久化 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"})
raw docstring

im.ttalk.agent.pause

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")
raw docstring

im.ttalk.agent.react

工具调用循环(从 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 评估结果缓存,确保每个工具调用
恰好触发一次观察回调(不重复)。
raw docstring

im.ttalk.agent.simple-agent

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")))
raw docstring

im.ttalk.agent.subagent.delegate

子 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_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] ...})
raw docstring

im.ttalk.agent.subagent.manager

子 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-optsmap并进子 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,反向依赖也不存在。
子 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,反向依赖也不存在。
raw docstring

im.ttalk.agent.subagent.tools

子代理异步工具组(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 换了,表里记着映射)。
子代理异步工具组(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 换了,表里记着映射)。
raw docstring

im.ttalk.agent.timeline

对话时间线与多分支(设计见 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")                    ;; 删分支(有子分支拒绝)
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