Skip to content

feat: step-code 终端 AI 编码助手探索分支架构快照(v0.1.2) - #107

Open
li-xiu-qi wants to merge 164 commits into
stepfun-ai:step-code-explorefrom
li-xiu-qi:step-code-explore
Open

feat: step-code 终端 AI 编码助手探索分支架构快照(v0.1.2)#107
li-xiu-qi wants to merge 164 commits into
stepfun-ai:step-code-explorefrom
li-xiu-qi:step-code-explore

Conversation

@li-xiu-qi

@li-xiu-qi li-xiu-qi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

概述

本 PR 是 step-code 终端 AI 编码助手的架构探索分支当前快照,从空分支起步、从 0 到 1 搭建完整架构,当前挂牌 v0.1.2(其后演进第三批,见下节)。与上游 main 分支差异极大(436 个文件,+84,586 行):

  • 155 个 commit,自上游预留的空探索根 3075d49 线性演进至今,保留完整的 worktree 功能分支合并拓扑
  • 173 个源码文件 + 189 个测试文件,覆盖 TUI、Agent、Provider、Tools、Session 全链路

Closes #103

核心架构

模块 内容
TUI(终端界面) Ink React 渲染、流式输出节流、Markdown 表格自适应、命令补全引擎(含参数级补全)、模型/渠道/会话/子 agent 选择器(全部视口自适应)
Agent(智能体循环) 主循环 + 子 agent 派生(跨渠道模型别名解析)、动态工作流(QuickJS 沙箱)、目标驱动模式、后台任务管理
Provider(多协议接入) Anthropic Messages / OpenAI Chat / OpenAI Responses / StepFun 双通道(API + Plan),全渠道请求前历史整形与媒体降级
Tools(工具系统) 文件读写、Bash、搜索、媒体读取(probe 模式 + region 分块)、Skill 加载、Hook 引擎、Checkpoint 回滚
Session(会话管理) 持久化、_index.json 索引缓存、恢复与回退、调试包导出、Wire 日志、Token 统计

架构级特性

  • 多模型支持:StepFun、Kimi、Claude、GPT 等,经渠道/别名配置解耦;别名指针消歧(多别名指向同一真实 id 不串渠道)
  • 多模态:图片读取、Alt+V 贴图(路径回读)、四通道统一媒体降级(超限自动换占位,不再毒化会话)
  • 子 agent 编排:spawn_agent 并行派生(只受并发/深度/展开规模三道真实闸门)、dynamic_workflow 条件分支与循环、/agents 只读下钻子会话
  • Skill 系统:声明式 SKILL.md + 懒加载,项目级/用户级/插件级三层
  • Hook 引擎:PreToolUse / PostToolUse / Stop / UserPromptSubmit / SessionStart
  • 权限模式:manual / auto / yolo 三档,工具级细粒度控制,只读白名单
  • 启动即配置自检:语法错误拒绝启动并给修复指引,语义错误降级警告,step doctor config 可校验
  • 四条安装路径:三端单文件可执行(tag 自动产出,附 sha256)、版本化安装包、预构建分支 npm 直装(约 12 秒)、开发分支安装自动构建

0.1.1 → 0.1.2 第二批演进

  • 子 agent 会话呈现重构:全局 /resume 只列主会话,新增 /agents 归属到会话上下文;子会话索引缓存根治 /resume 卡顿
  • 可靠性修复批:通知送达标记与消息本体同刻落盘(消除崩溃丢通知窗口);子会话锁 stale 检测(强杀后可恢复);/new /fork 换绑后台任务目录;edit_file 截断 diff 完整落盘
  • TUI 完善:选择器视口自适应 + 滑动窗口 + CJK 宽度修正;斜杠命令参数补全全覆盖
  • 配置体系/think /provider 切换写回配置;[tools.web] 缓存阈值可配置

完整演进记录见 CHANGELOG.md

v0.1.2 之后第三批演进(2026-08-08)

  • /team 多 agent 团队模式(本批主体):多任务并行改代码库——每任务独立 git worktree 物理隔离、写类任务范围两两互斥、依赖未合并系统硬拒启动、收编过六道门(空 diff 拒绝 / 已审阅 / tip 未动 / 依赖全并 / 无越界文件 / --no-ff,冲突自动中止救回)、agent 间信箱通信、退出落盘标记防 resume 复活、worker 失联自动标 blocked 可重派、merge 后自动清理工作间。配套内置 skill 引导 + drift 锚点测试。同日完成九任务实战试跑,试跑抓出的八处机制问题当日修复。
  • resume 模型别名修复:恢复会话时 provider 已按别名重建但 session.model 漏同步真实 ID,别名原样发服务端导致 404。
  • 子 agent 可见性:spawn 卡片显示角色与任务描述、/tasks 运行中优先且新触发在前、状态栏后台徽章带任务名。
  • 队列草稿保护:回合结束自动发送队列消息时不再误清输入框里正在编辑的内容(submit 加 fromQueue 守卫)。
  • goal 预算口径:工具描述写明默认不设上限,纠正模型顺手设预算的倾向。

测试与质量

  • 191 个测试文件,2570 个测试,Vitest 全绿(Test Files / Tests 双行核验)
  • CI 三平台(macOS / Ubuntu / Windows)通过
  • 合规检查:措辞审核 hook 常态化,许可证收录补全

文档

  • 中英双语 README、CHANGELOG、CONTRIBUTING、SECURITY
  • 完整配置说明(docs/en + docs/zh)
  • AGENTS.md(AI 代理行为规范)

与上游 main 的关系

本分支是独立探索,未合并上游 main 的后续变更。架构选择(如 Ink React、动态工作流、子 agent 编排)与上游可能有根本差异,建议作为新架构提案评审,而非增量功能合并。


分支: li-xiu-qi:step-code-explorestepfun-ai:step-code-explore
Commit 数: 150
文件变更: 436 个(+84,586 行)

li-xiu-qi and others added 30 commits August 2, 2026 12:12
- .step-code/agents/*.md 的 model 字段支持 [models.<别名>] 解析
- 命中别名时独立构造 provider,携带对应 base_url/api_key/capabilities/maxContextSize
- 自定义子 agent 角色(非 general/explore)自动进入主 agent system prompt
- --resume/-r//resume 恢复会话时,若存储模型别名失效自动回退 config.model
- 新增表格块渲染:按显示宽度对齐列宽,宽字符按 2 列计算
- 修复裸 URL / 自链接被打印两遍的问题
- 新增 markdownTable 回归测试
- 说明子 agent model 字段推荐写 Step 系列模型别名
- 补充自定义角色覆盖优先级、进入主 agent system prompt 的机制
- AGENTS.md 已具备能力列表同步子 agent 跨渠道解析、自定义角色可见性、恢复回退
- 移除别家 CLI/模型作为默认示例,统一 Step 官方口径
- 版本号对齐 0.1.0
- 安装说明以源码 clone + pnpm build 为主路径
- 补充 npm 全局安装(v0.1.0 起)作为后续注册 npm registry 后的方式
- package.json description 改为 Step 官方口径
- .env.example api_key 配置位置同步多渠道模型配置
- 交互文档补充 Ctrl+B 前台任务转后台说明
- 快速开始文档 provider 示例从 [providers.default] 改为 [providers.stepfun]
- 记录子 agent 跨渠道解析、自定义角色可见性、恢复回退
- 记录 UI 修复与文档口径统一
- 版本号 0.1.0
- 包含 provider 边界层、TUI 组件、工具循环、skill/MCP/插件系统、
  后台任务、自主目标、定时任务、动态工作流等完整实现
- 基于上游 step-code-explore 空分支从零构建
- 安装文档明确当前主要方式是源码 clone + pnpm build + pnpm link --global
- npm install -g step-code 改为 v0.1.0 起计划注册 npm 后可用
- 同步中英文 docs 与 step-code-install skill
- 搜索请求发到当前会话渠道 base_url 拼 /step_plan/v1/search,用当前渠道 key
- 主会话必须走阶跃渠道,否则搜索 404 / 鉴权失败
- 子 agent 的搜索跟随主会话渠道,不跟随子 agent 自己的 model 别名
- 404 / 连接失败的排查指引同步进错误分流说明
模型思考块只回签名、不回思考正文时(content_block_start[thinking] →
signature_delta → content_block_stop,零思考增量),过去 UI 完全没有信号,
只剩通用忙碌态 spinner,与请求卡死无从区分。

现在思考块的开始与结束作为事件独立上抛(thinking_start / thinking_end),
状态行在整个思考块期间显示「思考中…」(无正文时只有这一行标题),
思考块结束即收起。同时把忙碌态 spinner 的随机状态词从思考类换成中性
「处理中/推进中/…」,思考态由「思考中…」独家表达。

合并自 PeronGH 的 PR #1,整合到目标分支已演进的双语 docs 与新 CHANGELOG
结构中:docs/interactive.md(旧版单文件)已弃用,说明并入
docs/zh|en/interactive.md 的「推理过程显示」段。

Co-authored-by: Peron <peron_ap@icloud.com>
Closes #1
初始化三位贡献者:li-xiu-qi(项目作者与维护者)、PeronGH(无痕思考修复)、
ZouR-Ma(step-code-explore 分支探索的支持与指导)。约定:外部 PR 因目标分支
结构调整需改写提交时,以 Co-authored-by 保留原作者身份并在本文件登记贡献。
web_search / web_image_search 此前复用主会话模型渠道的 base_url + api_key,
主会话切到非阶跃渠道时搜索请求打到错误地址 404。搜索是阶跃平台专属能力,
与主会话用哪家模型在业务上无关,因此独立配置。

- config.toml 新增 [search] 段:三层结构([search] 通用兜底、[search.web]
  覆盖内容搜索、[search.image] 覆盖文搜图),字段全可选
- endpoint 解析优先级:专用段 → 通用段 → 主会话渠道;独立配置 url 视为
  精确意图不裁剪,兜底主会话时沿用旧的 /v1 归一化(向后兼容)
- 独立配置只给 url 没给 key 时 key 回退主会话 apiKey
- ctx.searchConfig 注入主会话(main.tsx)、子 agent(runner.ts,跟随主会话
  不跟随子 agent model 别名)、/reload 热重载(App.tsx 两处)
- 文档:configuration.md 中英 [search] 段说明,tools.md 中英 endpoint 解析
  与 api/plan 双通道说明(内容搜索 api+plan 双通道,文搜图仅 plan)
- 单测 tests/configSearch.test.ts 13 个全过
防漂移测试(updateConfigDrift)发现新增 [search] 顶层键未同步三处:
- doctor.ts CONFIG_TOP_LEVEL_KEYS 补 'search'
- updateConfig skill 内嵌 schema 表补 [search] 段(url/key + [search.web]/[search.image])

该测试是 update-config skill 自包含路线的漂移保险,加键不加表即变红。
顺带验证 questionPrompt 单跑 16/16 通过,全量下的那个失败是 TUI known flaky,与本改动无关。
修复一处不一致:package.json / src/version.ts 仍是 0.4.0,而 docs 与
step-code-install skill 已写 v0.1.0、tag 也已打 v0.1.0。

根因:早前改过这三处,但后续为整理分支历史、基于上游空分支重建
step-code-explore 时,git checkout <branch> -- . 拉的是改版本号之前的文件状态,
把那次改动覆盖了。tests/version.test.ts 只断言 package.json 与 version.ts
互相一致(同为 0.4.0 也算一致),因此未变红、未被发现。

- package.json version → 0.1.0
- src/version.ts VERSION → 0.1.0
- CHANGELOG [Unreleased] → [0.1.0] - 2026-08-02,正文改为从零重构首个探索版本
- tests/session/debugBundle.test.ts 硬编码断言同步

全量 pnpm test 1841 passed / 145 files,typecheck 通过。
Esc / Ctrl+C 中断后,历史里只剩一段残文,模型无法区分「自己出错」
与「用户主动叫停」,下一轮常出现道歉或重复解释上一轮。

runAgent 在 stopReason === 'aborted' 时追加一条 origin: 'injection'
的 user 消息说明中断事实。选 injection 而非新事件类型:该 origin 已有
五处消费点(UI 隐藏、压缩排除、undo 跳过、fork 边界、通知同类),
零新增基础设施;runAgent 又是 aborted 的单点出口,覆盖全部中断场景。
表格此前按内容自然宽度渲染、超宽交给终端硬折行,导致宽表溢出屏幕、
单元格不折行、中文列把其它列挤出后整体错位。

Markdown 组件加可选 width prop,由 App/MessageList/ExpandViewer 透传
终端列数;不在组件内取 process.stdout.columns,保持纯函数可测。

列宽分配两遍扫描(自然宽度 + 最长词兜底,30 列封顶),超出可用宽度时
按 grow potential 比例收缩、余数逐列补齐。单元格走 StyledSegment 线性化
后 greedy wrap——Ink 环境无法对 React 节点做 ANSI 折行,必须拆成
「提取样式」与「按宽折行」两步;CJK 逐字成断点,拉丁串按空格聚合。

边框开销按 3n+1 预留并渲染完整外框,两者必须配套:只画列间中缝会让
每行恒比终端窄 4 列。width 为 undefined 时走原自然宽度逻辑;可用宽度
不足每列 1 字符时回退原始 markdown。

新增 5 例覆盖无 width / 宽度充足 / 比例收缩 / 极窄回退 / 宽字符分配。
0.1.0 起每版发布 SEA 可执行文件与 npm tarball 两个产物,安装主路径从
「源码 clone + 构建」改为「从 Release 下载」,源码安装降为需要最新特性时的备选。

docs/zh 与 docs/en 的 installation、skills/step-code-install/SKILL.md
三处的安装/升级/卸载/故障排查全部同步;npm 全局安装仍标注为注册后可用。

package.json 加 prepack 钩子:npm pack 时自动 build,保证 tarball 内
dist/ 完整,装完即用,不依赖使用方本地构建。
机制早已完备(并行、后台、resume、跨渠道),但提示词里委派只有一行,
模型很少主动派生。本轮补齐三处缺口:工具描述过薄、内置角色描述从未
进提示词、委派纪律无并行意识。

- spawn_agent 描述扩为三段式(~180 → ~900 字符):写 prompt 四条、
  不该派生的三类情形、派生后守则。后台 anti-pattern 并入 schema describe
- AgentDefinition 增可选 whenToUse(兼容 when_to_use 蛇形写法),
  与 description 分工:一个答「是什么」,一个答「何时选」
- subagentListing 改为列全部角色(含内置 general/explore,此前被显式过滤),
  拼接 whenToUse,加 skillListing 同款三级预算降级
- 内置两角色 prompt 重写:补结果契约段;explore 要求给路径行号、
  查不到明说、区分事实与推断;general 补范围超限如实说明
- 子 agent 结果加状态头 subagent/status/session,error 附 resume 提示。
  格式化放工具层,SubagentResult.summary 保持纯净不污染落盘
- 删除 describeAgents 死代码(全仓无调用方,职能已由 subagentListing 覆盖)

角色清单刻意走 system prompt 而非工具描述:清单来自运行时扫描 markdown,
塞进 tools block 会让每次 .step-code/agents/ 变动 bust 整块 prompt cache,
代价不值得。
若干处注释只写了「做了什么」,没写「为什么这么做」,改动时容易误判意图。
这次把设计约束补进注释,并统一几处易误读的表述:

- projector.ts:说明消息序列修复的触发条件与不可跳过的原因。
- workingTips.ts / duration.ts:补上文案与时长格式的取值依据。
- AgentGroup.tsx:说明子 agent 分组的折叠规则。
- debugBundle.ts / logger.ts:注释内直接给出设计要点,去掉指向仓库外文件的
  失效引用(读者拿不到那些路径,指过去只是噪音)。
- App.tsx / HistoryPanel.tsx / OffsetViewport.tsx:模块间复用统一写作
  「沿用 / 复用」,「照抄」易被读成机械复制,与实际的按需借用不符。
- compaction-summary-gate.test.ts:断言里的示例路径改为与环境无关的
  D:/work/demo-repo,原先写的是开发机真实目录,换台机器读起来没有意义。
[compaction] model 此前只作为模型 id 透传给主会话 provider 的 stream({ model }),
只能在同一渠道内换模型。想让摘要走另一条渠道(不同 base_url / api_key / 协议)做不到:
跨渠道模型 id 发给主渠道端点会稳定 400,压缩每次失败等于上下文兜底失效。

现在该字段除模型 id 外还接受 [models.<别名>]:命中别名时走 resolveModelEntry 的
渠道/密钥回落链,用 createProvider 建独立 provider,摘要请求打该别名自己的端点。
未配置或裸模型 id 的行为与之前完全一致。

别名渠道构造失败时整体放弃覆盖、回退主会话模型,而不是退化成把跨渠道 id 发给主渠道——
后者是稳定失败,能压缩比压缩失败重要。

- src/provider/compaction.ts:新增 resolveCompactionBinding,四条分支 + 按别名缓存实例
- loop.ts:RunAgentOptions 加 compactionProvider,循环内压缩与溢出保命压缩两处消费
- runner.ts:子 agent 透传绑定
- main.tsx / App.tsx:组合根解析绑定;/reload 清缓存并按新配置重解
- compact.ts 零改动(它只在一处调 provider.stream,换实例在调用点做即可)

测试 7 例覆盖四条分支、缓存复用与降级路径。文档中英双语同步,内置 update-config
skill 的 schema 表一并更新(防漂移测试盯着)。
全量跑测试时总有 1 个用例超时,但每次挂的都不是同一个——三次分别挂在
promptInput 的两个不同用例和 markdownBodyRepro 上,跨文件漂移,而这些用例
单独跑全部稳定通过。这是调度竞争的特征,不是用例本身的逻辑缺陷。

成因:项目此前没有任何 vitest 配置,用的是默认 5000ms 超时,而 Ink 测试要
真实渲染组件树、靠 delay() 等待异步 flush。全套件 167 处硬等待累计 5.3 秒;
单个用例内也会累积,promptInput 那个逐字符删除 21 字符占位符的用例每步
delay(25),仅硬等待就 525ms,单跑约 1.2s。单跑余量看着够,16 核并发下互相
抢 CPU,放大 3 倍即撞线。

新增 vitest.config.ts 把 testTimeout / hookTimeout 提到 20s。没有去改那 167
处 delay——那是大规模无关重构,且硬等待本身是 Ink 异步渲染的合理写法,
真正缺的是与之匹配的超时配置。

放宽超时不会掩盖死锁或死循环,那类问题一样会超时,只是多等 15 秒;而假阳性
会训练人忽略红灯,代价更高。

验证:连续三次全量跑 1865/1865 全绿,墙钟 45-52s 与此前持平,
说明只抬高了上限、未拖慢正常路径。
react 与 react-reconciler 的 CJS 入口按 require 那一刻的 NODE_ENV 分流成 production /
development 两套构建,两者必须落在同一套。错配时 reconciler 调度静默失效——render()
正常返回、根组件函数从未被调用、stdout 零字节、不抛任何异常,TUI 表现为启动即空白屏,
而 -p 非交互模式与全部单元测试照常全绿。

两轮修复,方向相反,都必要:

① bin 入口换成不含 JSX、无任何静态 import 的引导文件 src/main.ts(先设 NODE_ENV 再
   await import('./cli.js'));原 main.tsx 改名 cli.tsx 内容不变;bin 仍指 dist/main.js,
   分发链路不变。加固三条:进程级首帧冒烟测试(经两轮反向验证)、结构断言、bundle
   打包期 esbuild define 静态折叠。

② 删除 src/env.ts。第一轮把它保留为「非 bin 入口的兜底」,注释写明「救不了 react 自身」
   但判定为可接受残留。实测证明它不是救不了,而是主动制造错配:在 bin 路径被引导文件
   抢先、在 bundle 路径被 define 折叠,两条都是 no-op;唯一真正生效的场合是直跑
   cli.tsx / dist/cli.js,而那里 react 已被 tsc 注入的 jsx-runtime 抢跑定型为 dev,
   它把 reconciler 单独掰到 prod,于是把「两包一致走 dev、完全可用」变成「错配、静默卡死」。

实测六组入口矩阵,唯一变量为入口路径与 NODE_ENV,外部一律 env -u NODE_ENV 清除环境值,
判据为 stdout 首 2000 字节,并用 require hook 逐包记录实际加载的构建文件:

  node dist/cli.js       修复前 react=dev / reconciler=prod → 0 字节;修复后同为 dev → 2000
  tsx src/cli.tsx        同上(与 dist/cli.js 同因)
  node dist/main.js      两包同为 production → 2000
  tsx src/main.ts        两包同为 production → 2000
  dist-bundle/step.mjs   define 折叠 → 2000(首次构建验证:此前 dist-bundle 是 8-01 旧产物,
                         define 加进构建脚本后一直没重新打包,那层加固从未被实际验证过)
  NODE_ENV=development tsx src/cli.tsx   两包同为 dev → 2000(证明「不设即一致」可用)

护栏方向随之反转:原有两例要求「cli.tsx 首个 import 是 env.js」,守的是一个会导致卡死的
约束。现为 9 例——保留引导文件的四条不变量(无静态 import、赋值早于动态 import、bin 与
dev 脚本指向),新增三条反向断言(cli.tsx 不设 NODE_ENV / 不 import 兜底模块 / src/env.ts
不存在)与一条 build-bundle.mjs 必须含 define。

验证:全量 2071/2071、160 个测试文件全过、typecheck 0、npm run build:bundle 通过、
措辞合规检查通过。firstFrameSmoke 确认真的执行而非因 dist/ 缺失被静默跳过。

注:本次为两个并行会话在共用工作区上的合并提交,AGENTS.md 与 CONTRIBUTING.md 的改动
含另一会话写的入口硬约束章节与调试章节;AGENTS.md 另含两处措辞合规修正(一处点名了外部
产品,一处把私有设计仓路径写进了公开仓)。
两个耦合的缺陷,都让压缩在长会话里失去兜底作用。

一、检查点只挂在 tool_use 一个分支

回合有六种结局,压缩只在其中一种末尾评估,于是三条路径完全绕过:纯对话轮(end_turn)、
用户 Esc 中断(aborted 直接 return)、以及新一轮 run 的第一个回合(循环顶部无检查)。
第三条影响最大——用户每次新提问都开新 run,上一轮结束时若已过线,新 run 的第一个请求
必然带着超限上下文发出;若模型实际窗口比配置的 max_context_size 更宽,接口不报 overflow,
这个状态不会自愈。现改为循环顶部「发请求前预检」,口径为 lastUsage.total + 尾部估算,
并删掉 tool_use 分支的重复调用(两处口径等价,留两处会在同一位置判两遍、连发两次摘要)。

二、压缩饱和时反复烧摘要请求

加了预检之后暴露出来的:当需要保留的最近消息本身就超预算时,每回合都压、每回合都压不到
阈值以下,于是每轮白发一次摘要调用。现加 compactionSaturated 标记,只在「确实压过了仍
超限」时置位——压不动(历史太短)不置位。这条区分是实测踩出来的:一开始两种都置位,
导致长 prompt 的子 agent 第一个回合即被判饱和、整个 run 不再压缩。饱和后停止本轮自动
压缩并明确提示用户改用 /compact 或 /new。

三、overflow 保命压缩不刷新状态栏

循环内压缩既发 notice 又发 usage,overflow 分支只发 notice——同一认知在同一文件里只
落实了一半,用户看到「已压缩并重试」而占用数字不动,像是压缩没生效。已补齐。

四、压缩不可中断(本项来自并行会话,与上述在 loop.ts / compact.ts 上耦合,故同批提交)

fullCompact 要等一次完整摘要请求,长历史实测可达数十秒,此前这段时间 Esc 完全无效,
手动 /compact 甚至没挂 abortRef。现将 signal 透传到摘要模型调用。中断的状态安全性建立
在既有事实上:fullCompact 对入参 messages 只读,新序列先在局部算完、由调用方
replaceMessages 一次性生效,因此中断只要发生在返回前,历史必然停在压缩前的完整状态,
不存在压缩到一半的中间态。顺带修了 overflow 分支的 abort 检查位置——原先放在 !acted
分支内,micro 恰好有收益时会绕过,用户先看到压缩提示、要等下一回合才真正停下;现在
中断优先于任何压缩结果判定。

砍了什么:不做「估算超限即拒发请求」的 100% 硬闸门(已有 overflow 恢复路径兜底,且
估算有约 6% 误差,硬闸门会把误判变成「明明能发却被拦」);不改精确 tokenizer;不做
异步预热压缩。

验证:typecheck 0;新增 6 例预检用例 + 7 例中断用例;全量 2071/2071。

仍未收敛,不要当已解决:落盘数据解释不了状态栏上那个远超上限的占用数字(最后一次压缩后
按估算复算与显示值差约 48 倍)。已排除落盘裁剪、缓存重复计算、估算低估三种解释,剩余
可能需运行时插桩才能定论。本次修的四处缺陷各自独立成立且有回归测试,但不构成那个数字的
完整解释。
@github-actions github-actions Bot added area/build scripts, .github, build/config files area/docs docs, AGENTS.md, README(s), CONTRIBUTING.md area/skills skills area/cli src/cli, src/commands, src/runtime, src/bootstrap, src/*.ts area/tui src/tui labels Aug 7, 2026
@li-xiu-qi li-xiu-qi changed the title feat: step-code 探索分支第二批演进(0.1.2)——/agents 子 agent 入口、会话体系性能与可靠性修复 feat: step-code 终端 AI 编码助手探索分支架构快照(v0.1.2) Aug 7, 2026
- Release 资产新增固定名 step-code.tgz 副本,latest/download 永久链接可装最新版
- 删除 CI dist-branch job 与 make-dist-branch.mjs(分支已在远程删除)
- 安装文档/快速上手/README(中英)与 step-code-install skill 对齐为四种安装方式
- 单文件可执行与 tarball 段落启用真实下载链接,去除「未发布」标注
- 新增 selectVisibleTodos:进行中全保留,最新一条已完成做进度上下文,
  剩余名额按原顺序填待办,待办不足时从最近回补已完成
- 替换 slice(0,5) 硬切:已完成在清单前部堆积时不再把进行中/待办挤出可视区
- 折叠行从 +N more 改为带隐藏条目状态分布(如 +3(1 已完成 · 2 待办))
- i18n 新增 todo.status.* 三个 key(中英),todo.more 改含 detail 占位
- 测试补 8 个用例:裁剪优先级 6 个 + 渲染 2 个
@li-xiu-qi
li-xiu-qi force-pushed the step-code-explore branch 2 times, most recently from 77c33a3 to 39651cf Compare August 8, 2026 12:23
…审阅收编

- 模式与工具:team_init/plan/spawn/merge/teardown/send/inbox/status 八工具 + /team 命令(init/status/exit/teardown),session 级开关,快照随会话落盘恢复
- 隔离:每任务独立 git worktree(.teams/worktrees/,写 .git/info/exclude 不动 .gitignore);写文件 per-worker 硬拦(runner 层 wrapWriteGuard);协调者在模式活跃时不能写代码
- 门控:build 任务范围两两互斥;deps 未全 merged 系统拒绝启动(代码强制,不靠自觉);依赖满足自动解锁
- 收编六道门:门〇空 diff 拒绝(防 worker 直提主仓绕过流程)/ 已审阅 / tip 未移动 / 依赖全并 / 无范围外文件 / --no-ff;冲突自动 merge --abort 救回;成功后自动清理干净 worktree(dirty 保留并提示)
- 生命周期:closedAt 落盘标记(resume 不复活,init 重进清标记保留状态);teardown quit_only 应急强退;lost 联动(BackgroundManager onLost → 仅 active 任务标 blocked,可 respawn 重派);completed 可 rework 返工
- 可指定基准:--repo 指挥其他仓、--base 指定基准分支(缺省当前分支,收编校验 checkout 防合错)
- 引导:内置 team skill(参数组合/反面教材/工作期用法/收编后验证/dist 错位运维知识)+ drift 锚点测试强制同步;init 报错意图识别(dir 误指 git 仓时引导改用 repo)
- TUI:状态栏 team 徽标、/team 忙时即时执行;subagent 请求支持 cwd/writeAllowRoot
- 测试:team 系列 30+ 用例(真实临时 git 仓跑 worktree/merge/门控/信箱/teardown)
…建成功但漏了同步,别名原样发给服务端导致 404 model_invalid;状态栏正常是因 displayName 走别名查表的另一路径
…先、同级新触发在前;状态栏 bg 徽章带最近任务名;submit 加 fromQueue 守卫,队列 drain 与空闲通知注入不再误清用户正在编辑的草稿
模型在 tool_use 分支陷入「看到相同结果 → 做出相同反应」的稳态时(每轮
assistant 消息与工具结果逐字节相同、零信息增量),此前只能烧到
maxIterations=500 大断路器才被拦下。本提交在 loop.ts 的 tool_use 分支
挂上确定性检测:

- 新增 src/agent/roundLoop.ts(纯函数,无 IO):fingerprintRound 取
  messages 尾部 assistant + tool_result 对拼稳定指纹(排除每轮必变的
  调用 id 与消息 ts;工具结果参与指纹,合法轮询结果在变则不触发);
  createRoundLoopDetector 维护 streak,连续相同第 3 轮 warn、第 4 轮 stop
- loop.ts 接线:warn 注入 user 消息 + yield notice(不打断,给一次机会);
  stop 走 notice + turn_done 收尾(模型行为问题不是系统故障,不走 error)
- i18n 新增 loop.roundLoop.{warn,inject,stop} 中英双语
- tests/agent/roundLoop.test.ts 穷举:指纹各成分参与度、streak 清零、
  合法轮询不误伤、is_error 参与、warn/stop 的 runAgent 级集成断言
- subagent.test.ts 压缩估算用例的工具参数逐轮区分:完全同构序列正是
  本检测要拦的形态,会在压缩触发前被硬停,测不到压缩路径
- CHANGELOG 补条目

完整设计见内部产品设计文档(跨回合零进展检测设计)。
…e 相等时 > 漏判索引过期(fork Windows CI 的 cleanup ttl_days 随机失败根因,upstream 同内容全绿佐证 flake);测试用 utimesSync 把直改 mtime 拨到未来消除时序依赖;主 SessionStore 同源同修
真实事故驱动:一个会话在同一条 bash grep 上原地复读 497 轮,直到撞 500 轮硬上限
才被中止,期间用户侧没有任何提示。回放该会话(1070 条消息 / 518 个回合对)确认
指纹算法本身有效(最大连续相同 streak 达 497,远超阈值),但暴露两个缺口:

一、round-loop 只认「与上一轮相同」。单槽 lastFingerprint 一旦不等就清零 streak,
于是周期≥2 的交替循环(A→B→A→B)每步都与上一步不同,streak 永远停在 1,检测一次
都不会触发;而「读 A 发现要看 B,读 B 又回头看 A」这种两态摆动在工具链路里比原地
复读更常见。改为大小 8 的滑动窗口 + 窗口内出现次数计数:出现 4 次及以上判 stop,
恰好 3 次判 warn。周期 1 的行为与原实现逐条一致,原有用例全部保留守住回归。

二、500 轮硬上限是唯一真正生效过的防线,且中间完全静默、撞线才硬停。现在在
maxIterations 的 50% / 80% 两档各注入一次自查提醒(notice + injection,照 roundLoop
的现成范式),阈值按比例计算而非硬编码,每档一轮交互只触发一次。提醒挂在循环顶部
而非 tool_use 分支——挂分支内会被 roundLoop.stop 的提前 return 吞掉。

i18n 同步:roundLoop 文案措辞从「连续」改为窗口语义,新增 turnWarning.mid / late 双语。
bash 工具此前对写操作零检查:write_file / edit_file 受 runner.ts 的 wrapWriteGuard
限制在 allowRoot 内,而 bash 的重定向、mv、rm、sed -i 一律直通,安全边界完全寄托在
模型自觉上(AGENTS.md 与 team skill 都只是把这一点写成告知)。

新增 checkBashWrite(command, cwd, allowRoot) 纯函数,引号感知地拆段后三档判定:

- A 档,目标可解析且越界 → 拒绝。相对与绝对路径统一 resolve 后再判定,路径遍历
  由 resolve 自身处理,不用正则去理解 ..;边界判定避开兄弟目录前缀陷阱。
- B 档,有写入迹象但目标不可解析 → 拒绝,并要求改写成 allowRoot 内的显式路径。
  覆盖变量、命令替换、eval、python -c / node -e / perl -e 内联写入、以及命令内
  cd 越界后的相对路径。这里不做 fail-open:放行等于没拦,而模型收到明确反馈会改写。
- C 档,无写入迹象 → 放行。动态路径判定附加「该段确有写入迹象」门槛,否则
  ls $HOME、grep $PAT f 这类只读命令会被误拦,误拦比漏拦更伤可用性。

生效边界:allowRoot 缺省或为空时一律放行。主 agent 没有 allowRoot,必须保持能往
仓库外写文件(既定行为);只有 team worker 场景才受约束。

本 commit 只落纯函数与单测(93 例,含误报防线;Windows 语义的路径断言按平台跳过),尚未接入
bash.ts 的 execute()。接线、ctx 传参与用户可见文案另做。

已知残留缺口:>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法不覆盖。
AgentGroup 的 backgroundHint 只挂在非 busy 的完整渲染路径(AgentGroup.tsx:215),
而 agentGroupRows 在 busy 时 return 1、面板压缩成单行摘要。前台派生子 agent 时主 agent
必然 busy,于是这条提示在真正需要它的场景永远不显示;只有后台派生且主 agent 空闲时
才出现——那时任务早已在后台,提示没有意义。

不是回归:Ctrl+B 的按键处理(App.tsx:1150)与文案 key 一直都在,缺的是 busy 摘要行
上的入口提示。

修法是在 busy 摘要行按 running > 0 追加一条紧凑提示,风格与已有的「· /tasks 查看全部」
一致,因此新增 agentGroup.busyBackgroundHint 而不复用带括号的 backgroundHint。
摘要行本身是 wrap="truncate",追加文本不会增加行数,agentGroupRows 仍返回 1——
AgentGroup.tsx:101 的注释记着一次「加 backgroundHint 导致帧高超预算越过 rows-1 红线」
的事故,这次刻意避开同一个坑,并在测试里断言行数不变。

补两条回归守卫:有 running 时含提示且行数不变、全部 done 时不含提示。
现象:「↓/j 选择下一条,tail 预览跟着切换」这条用例在 CI 上间歇失败,断言期望预览显示(ta),实际是(tb)。

先前把它归因为「等待不够」并把固定 delay 换成 vi.waitFor,归因错了——换完之后 windows CI 仍挂在同一条断言。真因是列表顺序本身不确定:TasksViewer 的排序是「运行中优先,同级按 startedAt 倒序」(TasksViewer.tsx:165-170),用例里两个任务同为 running,谁排在前完全由 startedAt 决定;而 makeTask 的默认 startedAt 是各自现取 new Date(),于是顺序取决于「两次调用是否落在同一毫秒」。落在同一毫秒时稳定排序保持声明顺序(ta 在前,凑巧满足断言),一旦跨过时钟 tick,tb 变新排到最前,初始选中项就成了 tb。

修法是显式给两个任务 startedAt 并让 ta 比 tb 新(偏移 50ms,两条仍算刚启动,时长显示不受影响)。诊断与修复都用注入法验证过:把 makeTask 的默认值改成「tb 晚 1 秒」以模拟 CI 条件,修复前这条用例失败、其余 19 条全过(与 CI 表现一致),修复后同一模拟下 20 条全过——说明对时钟的依赖被真正消除,不是碰巧通过。

vi.waitFor 保留:预览区确实要等一次 ink re-render 才出现,固定 20ms 在 CI 负载高时不够。它解决的是「等得够不够」,与排序不确定是两个独立问题,注释里已写清以免下次再误判。
设计文档「首次运行引导设计」第 20-21 行要求 select 步骤有四个选项,第四项是「查看文档,稍后手动配置」,i18n 的 firstRun.optionDocs 中英文案齐备,但该出口在实现上被三处叠加吞掉,实际彻底不可达(用户想跳过配置只剩 Esc):渲染只 map PROVIDER_OPTIONS(3 项 provider),第四项从未画出;方向键取模用 PROVIDER_OPTIONS.length,光标最大到索引 2,画出来也选不中;数字键路由里 idx === 3 分支写在 const option = PROVIDER_OPTIONS[idx]! 与 if (!option) return 之后,PROVIDER_OPTIONS[3] 恒为 undefined,按 4 被守卫提前 return,分支是死代码。修法对应三处:追加文档出口渲染行、取模改用 SELECT_OPTION_COUNT(渠道数 + 1)、把出口判断提到取值守卫之前。

选项 label 原为硬编码中文字面量('StepFun Plan 订阅' 等),i18n 里配套的 firstRun.optionPlan/optionApi/optionCustom 三条从未被用——后果不是提示缺失,而是英文用户在向导里看到中文。改为在 ProviderOption 存 labelKey、渲染时 t():PROVIDER_OPTIONS 是模块级常量,若在其中直接调 t() 会在 import 时求值把语言固化,运行时 /lang 切换不生效。硬编码的配置文档链接提示一并接线为新增的 firstRun.docsNotice。

顺带清理普查出的 26 条零引用文案(zh/en 双表对称删除,tsc 保证键集一致):app.goal/permission/provider/lang/plugin.arg.* 与 app.skill.list/none 共 22 条系重构遗留,实际替代者是 commands.ts 的 cmd.<命令>.sub.<子命令>;agentGroup.status.running/queued 在状态改用彩点后失效(done/error 仍在用);firstRun.optionPaste 属旧流程遗留、optionManual 与 optionDocs 语义重复。app.image.unsupported 同为零引用但保留并加注释:全仓没有任何 supportsImage/vision/multimodal 能力判断,该文案是「贴图给非多模态模型静默无提示」这个缺口的唯一线索。
此前 system prompt 里没有任何时间信息,模型只能拿训练截止日期当「现在」,后果是搜索时用错年份、写文档与 commit 时写错日期、判断「最新版本」基于过时认知,且它意识不到自己在猜。

为什么是「启动快照 + 诚实标注」而不是逐轮刷新真实时间:system 整块打 cache_control(provider/prepare.ts 的 buildSystemBlocks),逐轮改写会让 system 断点连同其后的 tools 与历史断点一起失效,等于每轮按全上下文重算 input token。所以这里放弃「保持准确」,改为明说它是启动快照、可能过时数小时、真要准确时间就去跑 date——换来 system 永久静态,精度反而可以给到分钟而不必压到天。

跨天是唯一不可容忍的过时(日期错一天会让写文档、判断时效系统性出错),用一条 injection 消息修正,复用 loop 既有注入通道(与 turnWarning、后台通知同形)。baseline 取「最后一条消息的本地日期」而非局部状态变量:runLoop 每个 prompt 回合重新调用,局部变量在回合边界就重置了,而跨天几乎总发生在回合之间——用局部变量等于永远检测不到。注入的提醒自身成为最后一条消息、ts 即今天,故天然只注入一次,无需 warned 标记;resume 后同样成立。

时间一律用本地时区并显式标注 UTC 偏移与 IANA 名。直接给 toISOString() 会错日期:实测 new Date(2026,7,9,7,30).toISOString() 在东八区得到 2026-08-08T23:30:00Z,每天 00:00 到 08:00 报给模型的日期都比本地早一天,而模型无法自行修正(它不知道用户在哪个时区)。crossedLocalMidnight 另加坏时间戳防御:Invalid Date 的日期键是 NaN-NaN-NaN,与今天必然不等,不拦掉会导致每轮都注入一次「日期已变更」。

三处注入点:主 agent(systemPrompt.ts,now 可注入以保证测试不随真实日期漂移)、子 agent(runner.ts,联网调查最依赖当前日期)、--print 纯净模式。25 个新用例:nowContext 20 个覆盖时区换算、半小时时区、跨午夜判定与坏 ts(时区靠运行时改 process.env.TZ 切换,其中 UTC 对照组兼作方法自验——本机是 UTC+8,若切换在 vitest 里不生效该组必然失败);loopDateChange 5 个覆盖注入接线本身,判定函数写对而接线接错的情形纯函数测不出来,并用「临时把注入条件短路」反向验证过——禁用后期望注入的 3 条失败、期望不注入的 2 条仍过。
bashWriteGuard 的三档判定纯函数与 93 个单测早已落地,但没有任何调用方:wrapWriteGuard 只拦 write_file/edit_file,bash 直接透传给 base。后果是 worker 一句重定向或 cp 就能写到工作间外,范围互斥只剩 team_merge 合并时的 diff 检查兜着——已经发生过踩穿。

接线点选在 wrapWriteGuard 而非原计划的 bash.ts execute():前者本就是 per-worker 写隔离的位置、已单测、已在 runner 里接到子 agent 的 hooks 上,改动只是加一个分支;接在 execute() 则要把 allowRoot 一路穿进 ToolContext,而这对非 team 场景没有意义。hook 层拦截还有个好处是能把守卫给出的 reason 直接回给模型,比工具层抛错更容易让它自己改对。

拦 bash 曾被担心会拦死 git(worker 要在工作间里提交),实测不成立:守卫只看命令行里的显式写入语法(重定向、cp/mv/rm/tee/sed -i/dd/truncate),git 子命令与 npm/npx 一律判为无写入迹象放行。接线前用 21 条 worker 典型命令量过误报面,唯一被误拦的是 ls -la > /dev/null——/dev/null 会被解析成绝对路径、天然落在 allowRoot 外,而这是最常见的丢弃输出写法,拦下去会卡死正常命令。故本 commit 一并加丢弃型设备白名单(/dev/null、/dev/stdout、/dev/stderr、/dev/tty、NUL、/dev/fd/N、/proc/self/fd/N),并留一条用例锁住「白名单不是所有 /dev/*」——dd of=/dev/sda 仍按越界拦。

测试:守卫层加 6 例(99 例)覆盖特殊设备与块设备对照;接线层加 5 例覆盖两端——git 提交/跑测试/丢弃输出必须放行,越界重定向/拷贝/删除与动态路径必须拦。两侧都做了反向验证:把 bash 分支短路后,期望拦截的 2 例失败、期望放行的 3 例仍过,确认用例测的是接线本身而不是恒真。

已知边界(未变):tee FILE < input 形态下取错 token 而漏判(tee FILE 与 cmd | tee -a FILE 能正确拦);>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法仍不覆盖。这些形态的越界写仍由 team_merge 的事后 diff 兜住。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build scripts, .github, build/config files area/cli src/cli, src/commands, src/runtime, src/bootstrap, src/*.ts area/docs docs, AGENTS.md, README(s), CONTRIBUTING.md area/skills skills area/tui src/tui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants