Skip to content

v1.8.0 —— 后台子 agent + 回合中插话

Choose a tag to compare

@javaside javaside released this 07 Aug 14:13
· 18 commits to main since this release

springai-agentdemo v1.8.0

v1.7.0 基础上的功能版(minor)。核心交付物仍是终端编码智能体 springai-code-tui

本版主题是**「不用干等」:两件事让你在 agent 忙的时候仍然能动——后台子 agent(派出去的活不占着回合)与回合中插话**(想说的话不用等它跑完)。第三件是记住上次用的模型

⚠️ 先读这一条:权限行为又变了

BYPASS(跳过全部权限检查)现在进了 Shift+Tab 的循环,不再需要 --dangerously-skip-permissions 启动参数。四档平权:默认自动接受编辑计划模式跳过全部权限检查

那个启动参数此前把两件事合并成了一个布尔:启动时能否起步于 BYPASS运行期能否切进 BYPASS。前者该受启动参数管,后者不该——为了临时放开一次就得重启进程,代价不合理。参数本身保留,语义收窄为「启动即起步于该档」。

这是安全性的净下降:此前不带那个参数启动的进程,运行期无论如何按不进 BYPASS;本版起按三下 Shift+Tab 就能进。而 v1.7.0 已经把这一档改成了真的跳过全部检查(内置底线与 ask 规则都不执行,只剩你自己写的 deny 规则)。

顺带修掉一句流传在三份文档里的假话:「BYPASS 也盖不住内置底线」。v1.7.0 起它就盖得住了,文档没跟上。

下载物仍是两个自包含运行包(解压即用,无需构建):

  • springai-code-tui-1.8.0-dist.tar.gz(macOS / Linux 首选)
  • springai-code-tui-1.8.0-dist.zip(Windows 首选)

两者内容一致:启动脚本(bin/)+ 主 jar + 全部运行期依赖(lib/)+ LICENSE/NOTICE/README。运行时界面版本标识为 v1.8.0

完整功能全景与⚠️安全声明见 v1.3.0 发布说明v1.0.0 发布说明;本文只列出相对 v1.7.0 的变化。


✨ 新功能:后台子 agent(run_in_background

TaskParallelTasks 多了一个 run_in_background 参数(默认 false不传时行为与从前逐字一致)。传 true 时它立刻返回一个 task id,子 agent 在一个常驻线程池里跑:主 agent 可以接着调别的工具、接着说话、接着追派任务,你也能立刻提交下一条消息——后台任务不计入 busy 闸门

> 让 explore 在后台把整个权限层的判定顺序摸一遍,你先接着改 README
  ⏱ 后台任务已启动  task_3f9ac21b · explore · 摸清权限层判定顺序
  好,我先改 README……

结果怎么回来(两条路,互斥)

靠一个「已消费」标记互斥,同一个结果绝不会送两遍

  1. 模型主动取——TaskOutput(task_id),可选阻塞等待。超时返回的是「任务仍在运行」,不是失败:把「还没跑完」说成失败,模型会去重派一个一模一样的任务。
  2. 自动送达——空闲且输入框为空时程序自动起一个新回合,把已完成任务的结果交给模型。多个任务合成一条通知。

「输入框为空」是这条判据里最实用的一半:你正在打字时它不会插进来抢走回合。想让它现在就送,把输入框清空即可。

界面

  • ⏱ 后台任务面板常驻输入框上方,列出全部后台任务( 运行 / 完成 / 失败 / 已终止 + 耗时 + 当前工具)。零任务时不占行
  • /tasks 打开管理面板:↑↓ 选择、Enter 展开结果、k 终止(先确认)、Esc 关闭。任何时候可开——后台任务的意义就在于「回合还在跑的时候也能看一眼」。
  • 后台任务只有起止各一行进滚动区,它的工具活动与结果正文都不进——否则会一行行插进你与主 agent 的对话里。
  • Esc 取消当前回合不碰后台任务。它们的生命周期与回合无关,这正是它们的用处。要停它去 /tasksk
  • /clear 与退出(/exit 与 Ctrl+C)终止全部后台任务,清理有界 2s。

⚠️ 权限:后台任务的 ASK 一律 DENY

这是这个功能最容易踩的地方。 后台任务走同一个权限引擎(判定顺序、规则、内置底线一字不差),差别只在最后一步:引擎判出 ASK 时,前台弹审批面板,后台直接拒绝

因为它的 turnId 早已过期,审批请求会被 UI 的迟到过滤当场静默拒绝——模型只看到一个没有理由的失败,然后对同一个操作反复重试直到耗光回合。所以这里主动拒绝,并在工具结果里写明原因、当前档位与正确的下一步。这与 BYPASS 档「永远不停下来等人」是同一条设计推理:一个不会停下来等人的执行路径,必须把「等不到人」变成一个模型看得懂、能据此改道的结果。

让后台任务真干活有三条路:事先写窄 allow 规则(精度最高)、切到「自动接受编辑」档(实用的中间档)、或只派只读任务explore / plan 天然如此)。

/continueListTasks:让模型也知道有哪些任务

  • /continue 会把当前进程正在跑、以及已完成但结果还没送出去的后台任务一并告诉模型,目的只有一个:别让同一批活跑两遍
  • ListTasks(新工具,仅主 agent)让模型随时列出本进程的后台任务及状态。为什么需要它TaskOutput 的 task id 只存在于会话历史里那条 Task 的返回值中,/compact 之后就被压掉了——没有它,模型能派后台任务却看不见自己派了什么。
  • 后台任务不跨进程-c 恢复上次会话时,上个进程的后台任务已经全部结束(只活在内存里)。此时 /continue 会提醒模型重新派发,而不是对着早已作废的 task id 干等。

✨ 新功能:回合中插话

一个回合可能有十几轮工具调用、跑好几分钟。而人在工具循环中途开口,十有八九是看它跑偏了想纠正——等事都做完再听,这句话就从「纠偏」降级成了「善后」。

过去只有两个出口:干等到底,或按 Esc 砍掉整个回合(连已经跑对的部分一起丢)。现在有第三条路:不打断回合,把消息插进下一次模型调用

你想 怎么做 等待
尽快让模型看到 直接 Enter(默认) 当前这一个工具跑完
等它忙完再说 /queue <消息> 整个回合结束
别跑了,听我的 Esc 立即;未送达的插话放回输入框

界面:没走的钉着,走了才滚动

和排队消息同一套:

  › 帮我看看这个模块
⏺ Glob  {"pattern": "*"}
  ⎿ Glob ✓
  › 换个思路             ← 送达那一刻才打出来,位置正好在工具结果之后
  好,那我改成……

  ⤷ 还没送出去的那句      ← 未送达:钉在输入框上方,随时看得见
  › 等下回合再发的那句     ← /queue 排队的,在它下面

两个面板的上下顺序即送达先后,行首符号也不同( vs )——不然分不清自己那句话什么时候会被听见。状态栏另有一个 · 插话 N 条 实时计数作为第二条反馈。

输入那一刻刻意什么都不往对话区打。 那时它还没送达,而对话区里的行事后改不了;打下去就永远停在「输入时」这个位置上,而它的真实位置在后面那条工具结果之后。于是屏幕顺序 = 发给模型的消息表顺序 = -c 回放顺序,三者一致。

消息插在哪里

合法位置只有一个:所有工具结果之后。落在 assistant(tool_calls)tool 之间就是悬空 tool_calls,下一次请求直接被网关 400。而会话存储层看不到这个位置——工具结果落库与「构建下一次 prompt」是同一步。所以注入挂在 ChatModel 装饰器层,那一层拿到的消息表已经配平。

插话送给模型时会被 [interjection] 包裹并附一句行为指引(大意:用户在任务执行中插话,未完成的工作仍在进行;与当前方向冲突就调整,否则先把手头的做完)。不包的话,模型在 tool 结果之后突然看到一条 user 消息,很容易判定「上一轮结束了,这是新任务」,于是丢下没做完的活。回合结束时这条插话会按锚点补进会话历史——否则 -c 恢复和压缩之后,历史上就会出现一次「无来由的转向」。

什么时候会自动回落成排队

  • 压缩中、以及回合已被 Esc 取消、只剩子 agent 在收尾——这两种状态不会再有模型调用,插话进去等于石沉大海。
  • 本条消息挂了技能/skill)——插话是一条纯 user 消息,带不了技能参数。

与后台任务的接缝

TaskOutput(block=true)故意等下去的(默认上限 300 秒),而这一等发生在主 agent 的工具线程上——期间主 agent 一次模型调用都不发,而模型调用是插话的唯一送达点。不处理的话,「后台」两个字会被 block=true 抵消得干干净净:界面一切正常(回合在跑、 面板照常显示),但你那句话要等满 5 分钟。故那个工具的轮询每 200ms 顺路查一次插话队列,有就提前收工。

已知边界:模型对那句行为指引的实际反应未经真实 provider 验证(冒烟用的是桩模型)。指引措辞目前偏保守,倾向让它先做完再理你。


✨ 新功能:记住上次用的模型

/model 选中的模型记在 <项目根>/.codetui/model.json(单键 lastModel,已被 .gitignore),下次启动自动恢复

  • 按项目隔离——不同仓库各记各的。你在这个项目里习惯用推理模型、在那个项目里用快模型,不用每次开机重选。
  • 与会话恢复正交:不带 -c 的默认启动照样恢复模型。这正是它存在的理由——「换个话题重开」和「换个模型」是两件事,不该被绑在一起。
  • 只记 modelId,不记 provider,与 /model 面板本身的选择粒度一致(面板也只能按 id 选)。*_MODELS 环境变量造成跨家重名时命中列表序靠前的可用家——已知限制,不是疏忽。
  • 该模型已不可用时(比如你临时注释掉了那家的 key),回退到首个可用 provider 的默认模型并提示一行,但盘上那条记录不清——于是不再选一次的话每次启动都会重复那句提示。这是有意的:清掉的代价是「你只是临时跑了一次没带 key,回头 key 加回来记忆已经没了」。
  • 写盘失败不影响使用,只在对话区提示「仅本次运行生效」。

与权限档位的处理刻意相反:权限档每次启动都回到默认,模型选择则记住。前者记错了会放大权限,后者记错了最多是模型不合手、一个 /model 就改回来。


🔧 其它变化

  • 启动不再等 MCP:MCP 连接改到后台进行。此前配了较慢的 MCP server 时,界面要等全部连上才出来。现在立刻可用,状态栏用 ⟳ MCP 连接中 N 显示进度,连上后工具自动进入可用集。
  • /continue 会把后台任务摘要拼进提示词:正在跑的、以及已完成但结果还没送出去的后台任务一并告诉模型,避免同一批活跑两遍。
  • ListTasks(新工具):让模型随时列出本进程的后台任务及状态。没有它的话,/compact 压掉历史之后模型就看不见自己派了什么。
  • 忙时 notice 降级为后缀:此前一条 notice(如「已取消当前回合」)会整条盖掉状态栏,把正在跑的回合指示一起挡住。现在忙时它退成后缀,运行指示始终可见。
  • 修文档假话:「BYPASS 也盖不住内置底线」这句在三份文档里流传,而 v1.7.0 起它就盖得住了。三份副本一并改正。

🧪 质量

mvn -pl springai-code-tui test1353 个用例,0 失败 0 错误,9 跳过。另有 9 个 pty 实机冒烟全绿(插话、后台任务、/clear、记忆、权限、模型记忆、编辑快捷键、附件、MCP 管理)——内联 TUI 的渲染缺陷单测原理上抓不到,只能开真伪终端读屏。

本版两个新功能的验收都压在冒烟上,这点值得写明:插话的接线(InterjectingChatModel.wrap() 那一行)摘掉后 1322 个单测一个都不红,只有冒烟里断「桩模型实际收到的请求体」那条会红——因为接线断掉时插话面板和状态栏计数照样正常显示,肉眼看界面完全正常,功能却已经死了。


⚠️ 安全声明

本版的权限变化是净下降,请重读文首那一条。 BYPASS 现在无需启动参数即可在运行期按进去,而它自 v1.7.0 起是真的跳过全部检查——内置底线与 ask 规则都不执行,只剩你自己写的 deny 规则。

后台子 agent 带来的新面:

后台任务在没有人看着的时候执行工具。 它走同一个权限引擎,且 ASK 一律 DENY(见上),所以默认档下它做不了需要审批的事。但切到「自动接受编辑」或写了宽 allow 规则之后,它就是在你不看屏幕的时候改文件。给后台任务写规则时请比给前台写得更窄。

其余(权限层不是沙箱、贴图会原样发给第三方模型 API、引用块防伪造不防提示注入)与 v1.7.0 一致。详见 code-tui README 的安全声明SECURITY.md


⬆️ 升级须知

  • 无破坏性变更:旧会话、旧 permissions.json、旧 mcp.json、旧技能目录照常可用。
  • 不传 run_in_background 时,Task / ParallelTasks 的行为与 v1.7.0 逐字一致。
  • Enter 在忙时的行为变了:此前是排到下回合,现在默认插话(尽快送达)。想要旧行为用 /queue <消息>
  • --dangerously-skip-permissions 的语义收窄为「启动即起步于 BYPASS 档」;「运行期能否切进 BYPASS」不再受它管——现在任何进程都能切。
  • 新增两个可选环境变量CODETUI_TASK_OUTPUT_TIMEOUT_SECONDSTaskOutput(block=true) 的等待上限,默认 300,钳在 [1, 3600])与 CODETUI_BACKGROUND_CONCURRENCY(后台子 agent 并发上限,默认 4,钳在 [1, 32])。都非法即回落默认,绝不崩启动。
  • .codetui/model.json 是新文件,已被 .gitignore。不想要这个记忆的话删掉即可,下次 /model 会重新写。

🔐 校验(SHA-256)

db06c6dcfbfc3bb2511f29c15bd4cd524e46d6d366a07eb24f6b4c6d94f381d7  springai-code-tui-1.8.0-dist.tar.gz
aef6b927ad4fb45584c39e025139b27dce9b1994d91423710a7b38243100f395  springai-code-tui-1.8.0-dist.zip
shasum -a 256 -c <<'EOF'
db06c6dcfbfc3bb2511f29c15bd4cd524e46d6d366a07eb24f6b4c6d94f381d7  springai-code-tui-1.8.0-dist.tar.gz
aef6b927ad4fb45584c39e025139b27dce9b1994d91423710a7b38243100f395  springai-code-tui-1.8.0-dist.zip
EOF

📄 许可

Apache License 2.0。发布包内随附 LICENSENOTICE(含所分发第三方库:Spring AI / Spring Boot / spring-ai-community 为 Apache 2.0,TamboUI 为 MIT)。本版未引入任何新的第三方依赖

环境:JDK 17+,macOS / Linux / Windows。