Commit Graph

33 Commits

Author SHA1 Message Date
caoqianming 6f2840a5d0 统一运行生命周期入口 2026-07-28 15:34:42 +08:00
caoqianming 7cfeebfa89 增强消息运行可靠性 2026-07-27 17:50:31 +08:00
caoqianming 85c918dc2f Add safe conversational workspace rename 2026-07-27 11:23:20 +08:00
caoqianming cac0bfcfe4 refactor(core): loop.py 传输健壮性层析出 llm_transport.py + _execute_tool_call 拆分
架构审查 P1#4:loop.py 把「provider wire 层瞬态故障自愈」与「agent 控制流」
塞在同一文件;_execute_tool_call 一个函数线性堆 10 个关注点(148 行)。

- 新增 core/llm_transport.py(438 行):畸形/必填 key 被吞/空响应检测、三类
  故障留痕、usage/delta 提取、robust_stream 重试策略(首败降级非流式 +
  salvage 可救当轮续)。取流两路径与 salvage 以 callable 注入——不 import
  loop,单测在 AgentLoop 实例上打桩 _collect_stream_once/_nonstream_once
  的现有缝隙原样保留
- loop.py 1158→812 行,回归 ReAct 主干:_stream_llm 只留上下文压缩准备
  (context 关注点),wire 健壮性委托 robust_stream;_collect_stream_once/
  _nonstream_once/_try_salvage_response 留在 loop(持 llm/emit 态 + 测试缝)
- _execute_tool_call 148 行拆为编排 + 4 个正交方法:_check_repeat_block
  (两道拦截)/_maybe_skill_model_switch(热切)/_repeat_feedback(登记+
  软提示)/_maybe_pptx_guard(产物机检)
- 4 个测试文件 import 指向同步更新(is_empty_response 等改公有名)

292 测试全过(loop 重试/空响应/repeat-guard/salvage 套件覆盖改动路径)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:44:22 +08:00
caoqianming fb214de960 fix(context): 窗口体量估算实测校准——50%压缩/85%折叠/占用环换 token 实测口径(bump 0.58.52)
diag 实测(scripts/diag_context_pressure.py 留仓):折叠机制有效、零撞硬上限,
但静态 CHARS_PER_TOKEN=2.5 对中文密集窗口低估近一倍——名义 85% 折叠线实际
~155% reliable 才触发(task 9a863424 冲到 40.7 万 tokens);代码密集反向虚高
(fe2d8b73 估 1.2M 实际 616k)。

修法是信号校准、不加新机制:
- context.py: estimate_window_tokens(provider 实报 usage 覆盖窗口主体,仅实测
  点后尾巴按 2.5 估)+ calibrated_chars_per_token(比值夹 [1.0,4.0] 带宽)
- session.py: last_measured_usage() 取窗口内最后一条实报 usage 的 assistant 行
  (best-effort 绝不抛;idx→内存 pos 映射校验 role)
- context_fold.py: maybe_fold 触发判定换 token 实测口径
- loop.py: 压缩门槛与 context_limit_chars(前端环)用校准比值,逐轮以
  sent_chars/prompt_tokens 刷新;校准态挪类属性兜默认,任何异常回退 2.5

已知残余:折叠后 run 在首次 chat 完成前崩掉会多折一次(摘要偏保守,原文全在
DB),不为此加持久化状态。run 中途折叠/超限自愈按 §5 无信号不实施继续搁置。

真实生产 task 只读验证映射与校准(drift 0.95x/0.51x/0.86x),286 测试全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:49:02 +08:00
caoqianming 62f3ddb658 fix(ui,loop): salvage 成功提示降噪——黄色警告降为灰色 info 行(bump 0.58.50)
用户反馈流内黄字「工具调用参数损坏但已就地抢救」吓人、疑影响后续对话。
核实零影响:salvage 只就地改写 arguments、当轮照常执行,对话历史与正常轮
无异;warn 是纯前端 SSE 展示事件,不入 messages、不回灌 LLM,刷新即消失。

处置取分级降噪而非全静默(上游返脏数据的事实要可见、可与 tool_salvaged
DB 留痕对上):
- core/loop.py:该 emit 加 level:"info",措辞软化为「已自动修复工具调用
  参数…继续执行」;
- web/static/js/chat.js:warn 渲染按 level 分流,info 走灰色 muted 行、
  不带 ⚠;黄色 ⚠ 只留给真打断本轮的路径(丢弃重试/熔断等)。

频发原因研究背书(DB 近7天,bad/chat 轮次):deepseek_v4.pro 14.5%、
flash 8.3%、unifyllm 5.9~7%,glm/sonnet 0 —— DeepSeek wire + unifyllm
网关两链路的流式 delta 乱序病,前缀实为同一调用自身内容的错位碎片
(与 0.58.24 定层一致);社区 opencode/goose/Cline/Roo-Code 同病且多数
closed-not-planned 未修,我方 salvage(85% 救回)+ 非流式重试已是最完整
处置 —— 频繁黄字本质是「救回成功」记录高频刷屏,降灰合理。

顺带:CLAUDE.md 文档节奏调整(bump/PROGRESS/CHANGELOG 改为一次 push
统一更新一次,不逐 commit;DESIGN 仍随触发它的 commit 走)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 08:47:44 +08:00
caoqianming f535dbaa9a fix(llm,loop): glm.pro52 空响应治本——禁 thinking 免推理烧穿输出上限 + loop 区分截断(bump 0.58.49)
失败面板 empty_response 簇 2026-07 主角 glm.pro52。探针 + DB + 代码三重定层根因链:
1. caps.max_output 是全仓死字段(只在 capabilities.py 定义,_build_kwargs 从不作为
   max_tokens 发出)→ 网关放任 glm-5.2 跑到自带 65536 输出硬上限;
2. glm-5.2 thinking 网关侧默认开(线上探针实测 reasoning_content=766>0,尽管 config
   thinking_mode:false —— 那开关是 glm.yaml 未做的 TODO,根本没透传);
3. 重任务(100k 上下文)上思考膨胀烧满 65536 被截断(finish_reason=length)、content 空
   → loop 判空响应整轮丢弃 + 同上下文无效重试(task 35744bea:5 次 empty 全
   tokens_out=65536,事件4=attempt2)。单任务烧 ~327k 输出 token。
与 opus48/deepseek 的网关 wire bug 不同根 —— 这是我方 max_output 死 + 未约束推理模型。

修(禁 thinking + loop 健壮化):
- core/llm.py _build_kwargs 加 family=="glm" 分支,据 thinking_mode 透传
  extra_body={"thinking":{"type":"enabled|disabled"}}(GLM body 级协议,与 OpenAI 的
  reasoning_effort 不同族;litellm zai provider 转发 extra_body)。当前 glm 档均 false=disabled。
  线上探针 A/B 实测:disabled 后 reasoning_content 766->0、正文/工具照常。
- core/loop.py 加 _finish_reason,空响应路径区分 length(截断,我方预算烧穿,重试无效)与
  wire 吐空,warn 措辞据实;record_empty_response units 记 finish_reason(JSON 免 migration)。
- 修既坏的 test_loop_empty_response(mock 缺 executor)+ 补截断措辞用例,61 测试全绿。
- 探针 diag_glm_empty_probe.py 加 monkeypatch 绕沙箱池 + PROBE_THINKING_OFF A/B 开关。

行为变化(知情):glm.pro52 用户现在拿禁思考的 glm-5.2(config 本就 thinking_mode:false)——
不再卡壳/空转,代价是硬任务少了推理链。遗留:max_output 对所有模型仍未生效(本次没盲发
max_tokens,怕截断大 write 的 args),需逐档评估安全值后另做。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:16:54 +08:00
caoqianming 3c60c5c736 fix(loop): 堵失败聚集两裂缝——必填key被吞畸形走非流式重试 + RepeatGuard连续同错streak拦(bump 0.58.33)
扫近14天工具失败面板 top13 定位两条反复烧 token 的裂缝:

① 必填 key 被吞畸形(面板 #3 edit 缺 path,14次/13task/9用户,最广):
定层坐实是流式 wire 乱序(path 值碎片被吞进 old_str 尾部,独立 path 键消失),
跨 provider,非模型漏参。关键区别是 JSON parse 成功 → 既不命中
_malformed_tool_calls(只抓 parse 失败)、salvage 也救不了,一路漏到 executor
才报错反复烧。新增 _toolcalls_partial_args(非空 dict 且 0<缺失必填<全部必填)
接进 _stream_llm attempt 循环走非流式重试(服务端一次拼好绕开乱序),
_log_partial_args 落 usage_events(kind=tool_malformed,签名 missing required keys)
纳入面板可观测(此前伪装成普通 [Error] 完全是盲区)。

② 换参数撞同一堵墙(面板 #2 edit old_str not found,单task 9次/24h 7次):
模型微调 old_str 重试,精确 args 指纹每次不同 → RepeatGuard arg 判据不累计。
补第二道判据:按工具的连续同类错误 streak(_norm_err 抹平路径/数字后签名相同、
跨≥2 不同 args),SOFT 注入定向提示、HARD 拦截一次并重置到 SOFT(非永久封死),
任一成功立即清零。

测试 test_loop_repeat_guard.py +12(err-streak 5 + partial-args 7),20/20 通过;
相邻 salvage/toolfail_malformed 23/23 通过,未碰坏既有行为。additive 内部机制,
不碰对外 API/DB 契约。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:22:42 +08:00
caoqianming 1685004f96 feat(loop): 空响应防御——provider 吐空自动重试 + warn 自停不静默 done + 面板留痕(bump 0.58.32)
task 2a1bc25d 复盘:unifyllm 网关对某档 Claude 偶发把 tool_use 漏成正文/直接吐空,回来的轮
tool_calls=[] 且正文空,loop 当"模型答完"静默 done(run_status=idle、无报错、无终态失败),
表现为"自己中断",只能人肉挖 DB 才发现。复测该 bug 现已消失/瞬态,但"空 tool_calls 直接
done"这个失败模式本身太隐蔽,任何 provider 未来吐一次空都会静默卡死,故加防御(对称既有畸形
salvage 链)。

- _is_empty_response(tc 空且 content 去空白为空;纯 tool_call 轮不误判)挂进 _stream_llm 的
  attempt 循环:空-空丢弃本轮走非流式重试(多数瞬态重发一次即恢复,用户无感)
- run() 收尾点分空-空分支:重试耗尽仍空不发静默 done,改发可见 warn「模型返回空响应,已停止,
  回复继续可重试」+ done(复用 stall 熔断自停话术,run_status 落 idle 可续),不引入终态 error
- _log_empty_response stdout + usage_events(kind=empty_response,cost 0)双写留痕;core/toolfail
  加第四段扫描聚成 tool=(empty)/kind=empty cluster(sample=model_profile 看哪个网关档在吐空),
  admin 面板 + 巡检邮件零改动自动带上(单次瞬态 <阈值不触发,跨 task 系统性吐空才冒头)
- 刻意不做内容嗅探式"narrated tool_call 特征→重试":marker 每次变且与正常正文高度重叠,假阳性
  (合法回答判坏反复重试)比罕见静默停更糟,那类只靠面板留痕兜、不改热路径行为
- tests: test_loop_empty_response.py 新增 7 件 + test_toolfail_malformed.py +2;顺手补回
  test_loop_malformed_retry.py 因 salvage(0.58.24)落地后失修的 loop.executor 桩。全 247 测试绿

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:29:49 +08:00
caoqianming e1d5bf6788 feat(loop): 畸形 tool_call arguments 就地抢救(salvage)省一次非流式重试(bump 0.58.24)
失败聚集面板同形态跨 write/edit/run_python/shell 复发(err=Expecting value char 0):
char-0 是 provider 在 wire 上把别处正文字节错标成该 tool_call 的 arguments delta 粘的
垃圾前缀,尾部真 JSON 完好并跑到串尾(定层已证 provider-wire 脏、拼接层无辜)。

命中畸形后先试就地抢救,而非直接整轮丢弃 + 降级非流式重 roll:
- core/salvage.py::salvage_tool_arguments(raw, allowed_keys) 纯函数,左→右扫每个 '{'
  试 json.loads(raw[i:]),第一个 parse-to-end 成功 + 非空 dict + 顶层 key ⊆ 该工具
  schema 参数名的即采纳。双护栏:parse-to-end 挡尾部截断的半个 JSON,key 白名单挡前缀
  里自洽的旁支 JSON。工具无关——allowed_keys 每工具从 executor.schemas() 现取。
- AgentLoop._try_salvage_response 全有或全无:本轮所有畸形都能抠出才就地改写 .arguments
  当轮继续,任一抠不出则一个都不动、原样走既有 _log_malformed_args + 非流式重试(零回退)。
- 抢救成功记 usage_events kind=tool_salvaged(record_salvaged_tool_call,cost 0),与
  tool_malformed 成对;toolfail 精确匹配 tool_malformed/run_error,salvaged 不进失败面板。

纯函数单列 core/salvage.py(不 import litellm)以便 tests/test_salvage.py 无依赖本机可跑
(loop.py 顶层 import litellm 本机导入期卡死);14 用例含 write/edit/run_python/shell 四工具
真前缀 + 截断/多键/旁支/尾残渣/空{}/跨工具白名单隔离等边界。盲区:provider-wire 抖动本机
复现不出、/verify 端到端跑不了,靠单测 + 生产灰度观察 tool_salvaged/tool_malformed 比。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:20:38 +08:00
caoqianming da289b2ce2 feat(ops): 畸形 tool_call 进工具失败聚集 + 巡检邮件只发活跃聚集(bump 0.58.19)
畸形轮整轮丢弃、messages 无痕,admin「工具失败聚集」原本看不见这类失败
(toolfail 只扫 role=tool 报错);且巡检邮件去重集是内存态,每次部署清零,
高频部署期每次重启都把 7 天窗口内早已安静的存量聚集重发一遍。

- 新增 record_malformed_tool_call:丢弃畸形轮时写 usage_events 一行
  kind=tool_malformed(units={tool,len,err,head,tail,tokens_in/out}),
  cost_cny 恒 0 —— cost 全 kind 合计且随任务展示,provider 抖动不算用户
  花销;真实 token 快照进 units 供反推浪费。DB 写失败静默不阻塞重试
- core/toolfail.py 加第二段扫描(usage_events kind=tool_malformed),聚成
  kind=malformed 同构 cluster,admin 面板/巡检邮件零改动自动带上;聚合
  逻辑抽 _add 闭包双数据源复用
- 巡检邮件发信前加 count_24h>0 过滤:已安静的聚集重启后不再重发,还在
  烧的重启后再提醒一次(刻意保留)
- 新增 tests/test_toolfail_malformed.py 4 用例(mock session_scope);
  RUN.md env 巡检段 + 故障兜底同步;PROGRESS 更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:04:22 +08:00
caoqianming 3c714b34c4 fix(loop): 畸形 tool_call 首败即降级非流式 + 损坏原文留痕(bump 0.58.18)
task 716ed3be 实测:deepseek-v4-pro 大参数中文 write(3~4k 字符)流式重 roll
同轮连挂 3 次全畸形,失败强相关而非独立随机,每章拖 3 分钟+,最后总靠非流式
兜底救场,3 次流式重试纯烧 token。

- _MAX_MALFORMED_RETRIES=3(3 流式+1 非流式)→ _MAX_MALFORMED_ATTEMPTS=3
  (1 流式+2 非流式),首败即降级;修掉旧 warn 计数可显示 "4/3" 的 off-by-one
- 新增 _log_malformed_args:丢弃前把损坏 arguments 首尾各 300 字符(ascii()
  转义)+ JSON 报错位置打 stdout [malformed] 标签 —— 畸形轮不入库,这是唯一
  留痕,供定性 provider delta 错位 vs 本地拼接 bug 及报 case 取证
- 新增 tests/test_loop_malformed_retry.py 4 用例;RUN.md 故障兜底同步
  (v4-pro 也中招 + [malformed] 取证口径);CHANGELOG/PROGRESS 更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:02:27 +08:00
caoqianming 66a515aa3d feat(context): §8.8 Phase 2 长会话中段折叠摘要 + 头部压缩指示环(bump 0.54.1)
- 折叠(0.54.0):run 起点窗口体量达 reliable_context×85% → core/context_fold.py
  把中段折叠为固定模板结构化摘要(路径/ID/数值原文保留),写 tasks.context_summary
  (migration 0021)+ 推进 context_base_idx;Session.load 注入仅内存「前情摘要」
  (_n_head/_base_idx 维护 idx 映射);摘要调用复用会话 prepare 后前缀 + 末尾追加
  指令,与上一轮 chat 缓存字节一致近全程 hit;失败零阻塞(warn+跳过);「新话题」
  硬重置 / web 清空对话顺带清摘要,gap 软重置保留
- 前端(0.54.0):新 SSE context_fold 事件(start 状态行提示 / done 流内分隔条);
  补 warn 事件渲染 —— 此前 loop 熔断/重复拦截提示在 web 端被静默丢弃
- 压缩指示环(0.54.1):头部金额撤下明面(挪 hover tooltip),换 SVG donut
  (绿<50% 未压缩 / 琥珀 50-85% 压缩区 / 红≥85% 达折叠阈值)+ 已整理×N;
  GET /v1/tasks/{id} 详情新增 context_window_chars/limit_chars/pressure/folds
  (additive,列表端点不加);折叠计费 kind="context_fold"(record_chat_usage
  加 kind 参数,折叠次数 = count 该 kind,零 migration);llm_start 事件补
  context_limit_chars,run 中实时刷环
- DESIGN:§8.11 新增「最小子循环 delegate」预留(上下文隔离而非多 agent 编排,
  按 diag 数据触发,无信号不实施);§1/§6 "不做 subagent" 收窄为编排型;
  §8.8 Phase 2 标  沉淀六条取舍;§7.2 SSE 清单补 warn/context_fold
- tests/test_context_fold.py 12 项(切点/映射/apply_fold/maybe_fold 打桩),
  全量 198 过;本地 DB 已升 0021

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:39:31 +08:00
caoqianming 463e282afd feat(web): run 长耗时可视化——reasoning 事件流 + 阶段跳秒指示(bump 0.43.0)
修"前端一直静止显示思考中被当成卡死":

- core/loop.py:新增 SSE reasoning{delta} 事件——thinking 模型分钟级推理
  原先 delta.reasoning_content 整段丢弃,前端零反馈;流式/非流式兜底两路都发
- chat.js:reasoning 渲染成灰色可折叠"思考过程"卡(直播态随流展开、正文
  开始折叠;历史消息里持久化的 reasoning_content 同款渲染)
- chat.js:活跃状态指示——TTFT/推理期占位段每秒跳"思考中/深度思考中 · Ns"
  (data-status + CSS attr),工具卡运行中 spinner + 跳秒、tool_result 定格
  为执行耗时;每轮 llm_start 重建占位段,工具轮之后不再空窗
- DESIGN §7.2 SSE 事件清单补 reasoning

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:02:31 +08:00
caoqianming c52f399f07 fix(guard): ppt 伪导出根治三层——平台机检门 + 禁令去菜谱 + 脚本 hint 纪律(bump 0.40.0)
0.35.1 复发复盘:模型照 SKILL 禁令里的"配方"装 cairosvg 手搓 SVG→PNG
整页贴图导出,文档层禁令两次被证不够。三层治理:

- 平台层:core/pptx_guard.py + loop 钩子——shell/run_python 本步新产出
  .pptx 命中"≥80% 页整版位图 + 全 deck 零原生文本"即在 tool 结果注入
  [产物机检 ERROR] 当场逼返工;判定刻意保守(≥2 页、表格/图表算原生、
  解析失败放行),tests/test_pptx_guard.py 8 例全过
- 禁令去菜谱:ppt SKILL.md:185/213、brief SKILL.md:26/103 否定式指令
  改"正面窄门"(唯一入口 + 报错就修/如实上报),删掉可照做的违规配方
- 脚本 hint 纪律:pptx_media / svg_preview / crop_images /
  fix_image_aspect / pptx_cli 共 5 处 pip install 提示改环境说明——
  打印给 agent 的安装指令就是执行指令

DESIGN 新增 §8.9(推翻 0.35.1"平台层暂缓"拍板);PROGRESS 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:40:54 +08:00
caoqianming 43775d531a feat(skill): skill 定向模型——ppt 触发自动热切 GLM-5.2(bump 0.39.0)
内置 skill 可在 frontmatter 声明 model: <family.variant>(ppt → glm.pro52)。
单一执行点:load_skill 命中 → run 内热切 self.llm/self.caps(下一轮生效,
记账/压缩阈值自动跟上)+ 持久化 tasks 双列 + emit model_switch 事件;切失败
降级 warn 原模型继续。send 侧档外静默降级对定向模型豁免(免费档第二条消息
不被降回 flash)。跳档位门控(产品决策);只信内置 skill(用户 skill 的 model
字段忽略,防绕门控);不自动切回。"开关"= frontmatter 那一行本身(registry
每 run 现扫,删行即停,per-skill 粒度)—— 曾实现 app_settings 表 + admin
总开关 + 建 task 预切,架构评审后砍掉(第二事实源/第二执行点),详 DESIGN §3.5。
前端:直播流 model_switch 插模型分隔线 + 顶栏下拉同步(档外定向模型临时补
option 如实显示)。新增 tests/test_skill_model_pinning.py。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:57:32 +08:00
caoqianming 1cfeb000a6 feat(web): ask_user 工具 — 回复里渲染可点击「方案确认」选项卡 + bump 0.14.0
agent 在真正的分叉点(2-4 个互斥方向且选择会实质改变后续动作)调用 ask_user,
前端渲染可点击选项卡:点一个即作为回复继续,或不点直接用文字讨论。

收窄定位防 agent 变爱问(高轮数烧 token 已知痛点),系统提示严格约束使用条件。
与轮次模型同构、无阻塞:ask_user 是虚拟工具(同 task_progress 范式),loop 检测到
本步调用它就提前结束本轮、不回灌 LLM;点选项=发该选项 label 作新用户消息,零额外
LLM 往返。选项落在 tool_calls.arguments 里,刷新页面按钮还在;已答的卡自动置灰。

- tools/ask_user.py 新增 AskUserTool;core/agent_builder.py 注册
- core/loop.py 加 ask_user 提前终止分支
- prompts/system/general_v1.md 加「方案确认约定」段
- web/static/js/chat.js buildAskUserCard + SSE/历史重渲特判 + sendMessage(overrideText) + 点击委托
- web/static/dev.html 加 .ask-user/.ask-option 样式

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:23:59 +08:00
caoqianming 8b6e66b006 feat(context): 压缩加"上下文压力门槛",短任务不压缩以护缓存+保信息
prepare_messages_with_stats 加 compact_threshold_chars:总 chars 未超阈值
则完全跳过压缩、原样发 —— 短任务 prompt 前缀逐轮字节一致、DeepSeek 前缀
缓存全程命中,且不白丢旧 tool 细节(context 预算还很空时无谓压缩=纯损失)。
超阈值才走原压缩逻辑。

- loop 按 caps.reliable_context × 0.5 × 2.5(char/token 粗折算)算阈值
  (flash ≈ 33 万 chars),_COMPACT_CONTEXT_RATIO/_CHARS_PER_TOKEN 可调
- compaction_skipped 进 stats / llm_start 事件可观测
- 默认 compact_threshold_chars=0 = 永远压缩(向后兼容)

背景:实测 task b27466a0 DeepSeek 缓存命中已 92-94%、滑动边界损失有限
(压缩函数确定性、旧消息压缩态稳定),故只补门槛、暂不改边界为阶梯式。
新增 2 测试(below/above 门槛),全量 105 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:42:18 +08:00
caoqianming 0df9e5fe3f feat(loop): 停机判据从"步数"解耦为"是否在推进"
max_iterations 降级为纯安全 backstop(flash 50→120 / pro 100→150),
不再当"轮预算"砍正经长任务;真正的空转防护改用进展信号:

- _RepeatGuard.record 多返 productive(净产出=非[Error]且非一字不差重复)
- _execute_tool_call 三个返回点都带 productive
- run loop 全局 _stall:整步全无净产出+1、任一净产出清零,
  连续 _STALL_LIMIT=8 步主动停([stopped: no progress]),
  比撞 backstop 早得多掐死循环,配逐指纹 HARD=4 双保险
- 撞 backstop / 空转停都 emit"回复继续可续跑"提示,不再静默停

诊断依据:task b27466a0"中途断了"实为撞 max_iterations=50 后干净停、
用户离开 25min 回来打"继续"续完(非崩溃);"步骤太长"=DeepSeek API
延迟 126-185s 而非工具(全<13s)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:22:02 +08:00
caoqianming 8150bf0b83 feat(tools+loop): 批量抓取 + 重复调用守卫 —— 治高轮数烧 token
DB 实测高轮数 task 的浪费来自三股根因(空 {} 风暴 / 报错重试 /
检索不收敛)叠加,且 loop 对重复调用零防护。本轮两味药:

药2 检索/抓取类 host 工具批量化(从工具形态减往返):
- web_fetch: url -> urls(1-10 并发,总预算 16000 按条分摊,单条失败不连坐)
- document_search: query -> queries(1-8 并发,批内去重,批量自动缩量防爆 context)
- document_download: file_name+kb_name -> items(1-10 并发,单条失败标 [Error] 不毁整批)
按「开发期不写兼容层」直接换签名、不留单数别名;skills/documents/SKILL.md 同步。

药1 loop 病理性重复守卫(core/loop.py::_RepeatGuard):
- 按 (工具名, 精确参数) 指纹跟踪「无产出重复」
- 只惩罚无产出(结果为 [Error] 或与之前一字不差);结果每次不同=有产出、清零,
  绝不误伤正常迭代(改脚本重跑 / 修 bug 重跑构建)
- SOFT=2 注入软提示;HARD=4 拦截不执行,逼模型换路
- 顺带堵 _malformed_tool_calls 漏空 {} 的洞(空 {} 每次返同句缺参错 -> 走 dup 被拦)

测试:tests/test_loop_repeat_guard.py(7 例)+ test_secret_host_tools.py 改新形态
并加批量/去重/失败隔离 3 例;相邻 24 测试全过。
诊断脚本留 scripts/diag_*.py 供复跑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:47:56 +08:00
caoqianming 8a7e0cd233 fix(loop): 工具调用 arguments 损坏时丢弃重试 + 非流式兜底,断投毒级联
deepseek-v4-flash 大参数工具调用(大 write/run_python,≈7K+ 字符)偶发把内容
碎片错位粘进 arguments 开头致 JSON 解析失败,或退化成空 {} 缺参。隔离批量验证
(干净上下文)非流式 8/8、流式 8/8 全干净 → 上游间歇抖动,概率低;真正放大成
灾的是 loop 把损坏的 assistant 消息入库 + 每轮重发,诱导模型继续学坏(投毒级联)。

- core/loop.py:_stream_llm 拉一轮后用 _malformed_tool_calls 校验 tool_call
  arguments 能否 json.loads,不能则丢弃整轮(不 append/不记账)重 roll(≤3 次),
  最后一次降级 _nonstream_once(provider 服务端拼,绕开流式错位)。流式收集逻辑
  抽成 _collect_stream_once,正常路径不变。
- core/executor_host.py / core/sandbox/tool_runner.py:缺必填参数早返
  「缺少必填参数 [...];请带齐 [...] 重新调用」,替掉暴露内部签名的
  TypeError missing N required positional arguments(host + docker 两路覆盖)。
- 文档:PROGRESS.md 加 2026-06-06 条;RUN.md 故障兜底加一行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:51:45 +08:00
caoqianming af2ad3cef1 feat(usage): 记账给前缀缓存命中折价 + 前端体现缓存命中/真实成本
排查"rust→PPT"task(flash,34 轮)发现累计 tokens_in 69.9 万里 88.6% 是缓存
命中,但 _fallback_chat_cost_cny 把命中段也按 input 全价算,记账虚高 2-3x。

- capabilities: 加 cache_hit_cny_per_mtoken(deepseek flash 0.1 / pro 0.2;
  0=不区分按全价兜底,绝不少记)
- usage: 成本公式拆三段「命中×缓存价 + (input−命中)×input价 + output×output价」;
  loop 把 cache_hit_tokens + 缓存单价透传进 record_chat_usage
- web: 不加 DB 列。app.py 加 _usage_aggregates(单查询 GROUP BY usage_events,
  复用列表 msg_counts 批量范式,无 N+1)on-the-fly 算每 task 真实成本 + 缓存命中,
  _task_dict 带出;dev.html 列表行显 ¥、顶栏 formatTaskUsage 显「tok·缓存命中%·¥」
- scripts: backfill_chat_cost_cache_discount.py 按 units 已存 token 重算历史
  cost_cny(只改成本列,默认 dry-run,--apply 落库)

折价只对新 chat 事件即时生效;历史走 backfill 脚本(部署后跑)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:26:09 +08:00
caoqianming 1c30a9e54e Reduce chat context token usage 2026-06-04 16:41:14 +08:00
caoqianming 48f99cf66d Stage C Step 1: Executor 接口骨架 + HostExecutor in-process backend
- core/executor.py: Executor ABC + ExecCtx(user_id/task_id/working_dir/cancel_check) + ToolResult
- core/executor_host.py: HostExecutor 包原 tools dict,统一三种错误为 ToolResult
- core/loop.py: AgentLoop 接 executor 而非 tools,_execute_tool_call 收成单条 call_tool 调用
- core/agent_builder.py: tools 装完后 HostExecutor(tools) 包一层,working_dir 透传 AgentLoop

接口形状与 DESIGN §7.5 #5 sketch (`call_tool(name, args, ctx)`) 完全一致,
backend 无关 —— Step 3 docker backend 接入时 AgentLoop 零改动,只换装配层。
行为零变化:smoke 4 分支(unknown/bad args/happy/schemas)全过,unittest 1/1 PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:07:55 +08:00
caoqianming 972f36db20 fix(loop): tool message append 补 name 字段 + backfill 历史 — 修历史 task 重开后 seedream banner/chip 不展示
session.append 的 tool 消息只存 role/tool_call_id/content,没 name;前端历史渲染依赖 payload.name 判断产物工具白名单 + 抽 elapsed banner,刷新后两者全黑(流式正常因为 SSE event 单独带 name)。scripts/backfill_tool_message_name.py 按 task 走 assistant.tool_calls 建 tool_call_id→name map 回填,dry-run 默认,--apply 真写,幂等。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:14:23 +08:00
caoqianming a3acb97079 feat(loop+ui): LLM 调用切 streaming — cancel 秒退 + 前端打字机 + 发送/停止合并单按钮
- core/llm.py: 加 chat_stream() generator(stream=True + include_usage),
  generator finally 关底层 httpx 连接;_build_kwargs 抽出来 chat/chat_stream 共用
- core/loop.py: 主循环 _stream_llm() 流式迭代,chunk 间 poll cancel 命中 break,
  litellm.stream_chunk_builder 拼回 response 给 tool_calls 解析 + usage 记账;
  content delta 即时 emit text 事件激活前端打字机渲染
- web/static/dev.html: chat-send + chat-cancel 合并 chat-action 单按钮,
  setActionMode(idle/streaming/cancelling) 切态;streaming 期间 Enter 不触发停止
- cancel 延迟从「整轮 generation 时长」(几十秒)降到「单 chunk 间隔」(100ms 级)
- 文档:DESIGN §3.1 + API 表 + risks 表翻转 tradeoff;RUN 接口 + 故障兜底同步;
  web/app.py docstring 对齐;PROGRESS 加条目 + 文件清单行数

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:46:54 +08:00
caoqianming 781a216ca6 model: 同 task 内切模型(c 模式 task 级 / A 粒度)+ usage_events v2 表(0006); GET /v1/models; 前端顶栏下拉 + 历史 model 切换点小标
- DB(0006): messages 加 model_profile 列(assistant 行有值); 重建 usage_events 表 v2 形态(event_id/user_id/task_id/message_id/kind/model_profile/units jsonb/cost_usd + 三索引), 0004 删的旧 schema 字段不够多态; tasks.tokens_prompt/completion/cost_usd 保留作粗概览
- ModelCapabilities 加 display_name; deepseek_v4.yaml flash/pro 各填名
- GET /v1/models: 扫 config/models/*.yaml 列可选项(profile/display_name/family/thinking_mode/is_default); POST /v1/tasks + PATCH 接受 model_profile(不传 → cfg["default_model"]; 校验走 ModelCapabilities.load 失败 400)
- build_agent: resume 时优先 task.model_profile 而非 cfg default; AgentLoop 加 user_id 透传, 每轮 assistant 入库后调 record_chat_usage(litellm cost map 算钱, 失败吞掉 emit warn 不阻 loop)
- core/storage/usage.py 新文件: record_chat_usage(双写 messages.tokens_in/out + model_profile + insert usage_events 一行)
- session.append() 返回 message_id(供 usage 关联)
- 前端 dev.html: chat-meta 加模型下拉(切了 PATCH + running 中提示"跑完后生效"); 新建对话框 modal 加 nt-model select; renderMessages 按 model_profile 切换点画小标 "── DeepSeek V4 Pro ──"
- CLAUDE.md: 加"开发测试期 / 不删现有数据 / DROP COLUMN 两种情况"规则
- DESIGN §7.4 schema 加 messages.model_profile + usage_events v2 段; PROGRESS 加 0006 条目 + 文件清单

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:43:13 +08:00
caoqianming bf41631437 core(run cancel): POST /runs/{rid}/cancel + AgentLoop 协作式 cancel + dev SPA stop 按钮
落地 DESIGN §7.2 原标"待"的 cancel 路由 — 等待回复 / LLM 操作时也能中断。

- broker 加 request_cancel / is_cancelled / clear_cancel(per-run threading.Event)
- AgentLoop 加 cancel_check 回调,每轮 LLM 前 + tool_calls 之间 poll;命中给
  未执行 tool_call 补 [cancelled by user] tool result 保 LiteLLM 协议,emit
  cancelled event
- 单活 gate + 启动 reaper 扩到 running | cancelling
- BG 装 cancel_check + 终态判 cancelled/ok + finally clear flag
- dev SPA stop 按钮 + cancelled badge + fetchSse try/finally 失败路径复原 UI

LLM 同步 call 本身不可中断,最坏等当前一轮跑完(通常几十秒)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:42:45 +08:00
caoqianming 375bb2999c core: loop 事件流化 (sink 接口, §7 A 阶段)
loop 不直接 console.print —— 改成 sink.emit({type, ...}),sink 决定怎么呈现。
新增 ConsoleEventSink 接管 spinner / [in N out N] / assistant 文本 / tool>(args) / 结果预览,
CLI 行为零回归。后续接 SSE 时只换 sink 实现,loop 不动。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:59:32 +08:00
caoqianming 72d2b64c40 core/ui: 抽出语义化 console 主题, 调用方去硬编码颜色
新增 core/ui.py 集中定义 Rich Theme:
- 语义样式名: user / assistant / tool / ok / warn / err / info / muted / accent
- 在黑底终端上 readable, 弱化用 grey 而非 dim, 强调走 bright_*
- make_console() 统一应用主题, 以后改主题只动这一处

cli.py / main.py / core/loop.py 把内联的 [red] [green] [blue] [yellow]
[cyan] [dim] 等替换为语义样式; 调用 make_console() 取代 Console()。
2026-05-07 16:10:11 +08:00
caoqianming 61ff98bb79 loop: thinking spinner 显示耗时 + 累计 token,每轮回打成本一行
spinner 文本由后台 daemon 线程每 100ms 用 status.update() 刷新,显示
'thinking... 1.3s  ctx 12,345 tok'。每轮 LLM 返回后追加一行 dim
'[in N  out N  t Xs]',留痕便于回看本轮成本。全 ASCII。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:05:44 +08:00
caoqianming 80f9934f8f loop: 用 rich.Markdown 渲染 assistant 输出
之前 console.print(f"...{content}") 只解析 [tag] 标签,不识别 **bold**、表格、列表
等 markdown 语法,LLM 回复会以原文显示。改成单独打印前缀后再 console.print(Markdown(content))。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:19:46 +08:00
caoqianming 3a66849953 Initial import: zcbot personal task agent
DESIGN.md / PROGRESS.md 落地 Phase 1-3:
- core/: LiteLLM 封装 + ReAct loop + 会话持久化 + Anthropic skill registry
- tools/: read/write/edit/glob/grep/shell/run_python/load_skill (Hybrid 范式)
- skills/coding | proposal: WHY+WHAT 风格 SKILL.md
- skills/ppt: 完整渐进披露 (SKILL + 4 references + 3 scripts)
  · 借鉴 hugohe3/ppt-master 的两阶段 + spec lock 思路
  · MSO_SHAPE 图标体系 + 安全区 + 越界检测
  · 默认商务红主题 (#C00000 / #E15554 / #FFC107)
- config/models/: DeepSeek V4 flash/pro 档案

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:02:59 +08:00