fix(fs-search): keep broad glob samples representative

Remove the duplicate model-facing list tool from this branch; directory orientation remains available through bash ls. Keep the glob sampling fix, add a real ACP composition snapshot, and narrow the decision record to the shipped bug fix.
This commit is contained in:
NI0317
2026-07-28 14:42:59 +08:00
parent 4b5b22d42f
commit 57cf137918
55 changed files with 775 additions and 1171 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.md
2026-07-27-glob-sampling.md: c583f0cd8110684a94d0cd04f5cf2ae861ce7aa3
2026-07-27-glob-sampling.zh.md: 97025a7714237fc716dee745a858140cd783ba2d

View File

@@ -0,0 +1,45 @@
# Agent Note: Sample over-cap glob results across the tree
Status: implemented
English | [中文](2026-07-27-glob-sampling.zh.md)
## Problem
Asked what a workspace contained, an agent described one subfolder as if it were the whole project. The workspace held 22 top-level entries and 11,485 files. `glob {"pattern":"*"}` matched 10,030 paths, but all 100 inline paths sat under one recently unpacked subtree, so the model never saw the other 21 entries.
Three individually valid behaviors composed into the false impression. A glob without `/` matches basenames at any depth, so `*` means every file in the tree rather than the shell's current-directory expansion. Ripgrep's `--sort=modified` is ascending, so an archive's restored old timestamps put that subtree first. The inline page then took the head of that order without saying that it represented only one concentrated slice.
## Decision
A result that fits within `globMaxResults` remains complete and byte-for-byte modification-time ordered. An over-cap result is sampled round-robin across the complete result's top-level entries: every entry receives one slot before any receives a second, exhausted groups drop out, and relative order remains stable within each group. Grouping is relative to the actual search root, including an explicit `path`.
The footer states that the page is a cross-entry sample rather than the modification-time head, reports how many top-level entries it reaches when that fact adds information, and preserves the complete sorted list in the spill artifact. When more top-level entries exist than inline slots, it tells the model to narrow `path`.
The prompt and schema also state that a pattern without `/` matches at any depth and that glob returns files, never directory entries. Directory orientation remains ordinary shell work in deployments that expose the model-facing bash tool: use `ls` for one directory, and glob for a named file-path pattern across the tree. `ctx.fs.listDir` remains an internal provider primitive used by skill discovery; this decision adds no model-facing `list` tool.
## Alternatives considered
**Keep the modification-time head and only warn about concentration.** Rejected after measuring the failure shape. A warning asks the model to distrust the only paths it received; representative data fixes the answer directly.
**Sample every result.** Rejected. A complete result loses nothing to truncation, so modification-time order remains useful for age-oriented questions. Sampling begins only when the head stops describing the whole.
**Switch to newest-first order.** Rejected. It merely changes which concentrated subtree can dominate and removes the existing oldest-first contract without making a capped page representative.
**Sample only past a skew threshold.** Rejected. No current evidence supports a deployment-wide threshold, and the model could not know which ordering contract applied. The existing cap is the explainable transition.
**Balance recursively below the top level.** Deferred. First-segment balance fixes the observed failure; deeper balancing needs an unsupported depth-versus-breadth policy.
**Add a model-facing `list` tool.** Rejected after implementation review. The default coding composition already exposes general bash and the model understands `ls`; a duplicate tool would add permanent schema/prompt tokens plus ordering, pagination, symlink, escaping, UI, and snapshot contracts without a distinct security or policy benefit. Thin deployments without a model-facing bash tool do not gain directory orientation from this change.
**Reject `*` or silently anchor separator-free patterns.** Rejected. The same basename-at-any-depth behavior makes `*.ts` useful across a tree. Documenting the rule preserves working ripgrep semantics.
## Consequences
An over-cap glob page no longer answers age-order questions from its inline paths; its footer says so, and the spill artifact retains the complete sorted view. Sampling balances only the first segment beneath the search root, so a deeper hot subtree can still dominate within one top-level entry.
The tool surface does not grow. The fix changes glob's prompt, schema description, canonical output (`root` records the sampling basis), and over-cap Native rendering while leaving fitting results unchanged.
## Testing
Package tests pin concentrated and flat results, explicit roots, more groups than the JavaScript argument limit, exhausted groups, fewer slots than groups, and paths outside the workdir. The `fs-glob-sampling` ACP scenario boots the real Loader/app/sandbox-bash composition and executes the real search plugin against a deterministic `rg` process fixture; its result spans four top-level entries instead of returning one subtree's head.

View File

@@ -0,0 +1,45 @@
# Agent Note: 跨目录树采样超出上限的 glob 结果
Status: implemented
[English](2026-07-27-glob-sampling.md) | 中文
## 问题
用户询问工作区包含什么内容时,一个 agent智能体把某个子文件夹描述成了整个项目。该工作区有 22 个顶层条目和 11,485 个文件。`glob {"pattern":"*"}` 匹配到 10,030 条路径,但内联显示的 100 条路径全部位于一棵近期解压的子树中,因此模型完全没有看到其余 21 个条目。
三个单独看都合理的行为叠加后造成了错误印象。不含 `/` 的 glob 会匹配任意深度的文件名,因此 `*` 表示目录树中的每个文件,而不是 shell 对当前目录执行的展开。Ripgrep 的 `--sort=modified` 按升序排列,因此归档包还原出的旧时间戳会让该子树排在最前。随后,内联页面直接截取这一顺序的前部,却没有说明它只代表集中于一处的切片。
## 决策
未超过 `globMaxResults` 的结果仍保持完整,且按修改时间排序的内容逐字节不变。超过上限时,系统会在完整结果的顶层条目之间按轮转方式采样:每个条目都先获得一个位置,之后才有条目获得第二个位置;已经用尽的分组会退出轮转;各组内部的相对顺序保持稳定。分组始终以实际搜索根为基准,显式指定 `path` 时也如此。
footer 会说明当前页面是跨条目的样本,而不是按修改时间排序的前部;当顶层条目覆盖数能提供额外信息时,还会报告该数量;完整排序列表仍保存在 spill 产物中。若顶层条目数量超过内联位置数footer 会要求模型缩小 `path`
提示词与 schema 还会说明:不含 `/` 的模式会匹配任意深度glob 只返回文件,绝不返回目录条目。在向模型暴露 bash 工具的部署中,目录定位仍由普通 shell 操作完成:查看一个目录使用 `ls`,跨目录树按指定文件路径模式查找则使用 glob。skill技能发现流程仍将 `ctx.fs.listDir` 作为内部提供方原语使用;本决策不会新增面向模型的 `list` 工具。
## 考虑过的替代方案
**保留按修改时间排序的前部,只警告结果过于集中。** 测量实际故障形态后否决。警告只会要求模型怀疑自己拿到的唯一一批路径;具有代表性的数据能直接修正答案。
**对所有结果采样。** 否决。完整结果没有因截断损失任何信息,因此按修改时间排序仍有助于回答关注新旧时间的问题。只有当截取前部已经无法描述整体时,才开始采样。
**改为最新优先排序。** 否决。这只会改变哪一棵结果集中的子树可能占据主导;既取消了现有的最旧优先契约,也没有让受限页面更具代表性。
**仅在偏斜超过阈值时采样。** 否决。目前没有证据支持适用于所有部署的统一阈值,模型也无法判断当前采用的是哪一种排序契约。现有上限是可以清楚解释的切换点。
**在顶层以下递归平衡。** 暂缓。按第一路径段做平衡已经修复观测到的故障;更深层的平衡需要一套尚无依据的深度与广度取舍策略。
**新增面向模型的 `list` 工具。** 实现评审后否决。默认编程组合已经提供通用 bash模型也理解 `ls`;重复工具会永久增加 schema 与提示词所占的 token并引入排序、分页、符号链接、转义、UI 与快照契约,却没有独立的安全或策略收益。不向模型提供 bash 工具的精简部署也不会因本次改动获得目录定位能力。
**拒绝 `*`,或在不含分隔符的模式前静默加上根目录锚点。** 否决。同样的「在任意深度匹配文件名」行为使 `*.ts` 可以有效地跨目录树搜索。记录这条规则能够保留正常工作的 Ripgrep 语义。
## 影响
超过上限的 glob 页面无法再根据内联路径回答按时间判断新旧的问题footer 会明确说明这一点spill 产物仍保留完整的排序视图。采样只平衡搜索根下的第一路径段,因此某个顶层条目内部较深处、结果密集的子树仍可能占据主导。
工具接口不会扩大。此修复会更改 glob 的提示词、schema 描述、规范输出(`root` 记录采样基准)以及超过上限时的 Native 渲染,未超过上限的结果保持不变。
## 测试
包测试锁定了结果集中与扁平两种情况、显式根目录、分组数超过 JavaScript 参数个数上限、分组耗尽、位置数少于分组数,以及工作目录以外的路径。`fs-glob-sampling` ACPAgent Client Protocol场景会启动真实的 Loader/app/sandbox-bash 组合,并让真实搜索插件对接确定性的 `rg` 进程 fixture测试前置数据其结果覆盖 4 个顶层条目,而不是只返回某棵子树的前部。