From 57cf1379186ef04c56c2688dbb48ebed8bc2d0c8 Mon Sep 17 00:00:00 2001 From: NI0317 Date: Tue, 28 Jul 2026 14:42:59 +0800 Subject: [PATCH] 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. --- .../2026-07-27-glob-sampling.i18n.yaml} | 6 +- .../bug-fix/2026-07-27-glob-sampling.md | 45 ++ .../bug-fix/2026-07-27-glob-sampling.zh.md | 45 ++ .../2026-07-27-directory-listing-tool.md | 109 ---- .../2026-07-27-directory-listing-tool.zh.md | 109 ---- docs/config-catalog.md | 4 +- docs/tool-catalog.md | 4 +- .../acp-agent/fs-search.cordis.snapshot.yml | 24 + examples/acp-agent/fs-search.cordis.yml | 13 + examples/acp-agent/tests/acp.snapshot.ts | 19 +- .../acp-agent/tests/fixtures/fs-search-bin/rg | 8 + .../system-prompt.expected.md | 23 - .../tool-schemas.expected.json | 17 - .../both-mode-turn/system-prompt.expected.md | 23 - .../both-mode-turn/tool-schemas.expected.json | 17 - .../code-mode-turn/system-prompt.expected.md | 23 - .../system-prompt.expected.md | 23 - .../system-prompt.expected.md | 2 - .../tool-schemas.expected.json | 17 - .../snapshots/fs-glob-sampling/input.json | 7 + .../snapshots/fs-glob-sampling/session.jsonl | 24 + .../stdout.expected.jsonl | 2 +- .../system-prompt.expected.md | 29 + .../tool-schemas.expected.json | 517 ++++++++++++++++++ .../tests/snapshots/fs-list/input.json | 7 - .../tests/snapshots/fs-list/session.jsonl | 32 -- .../snapshots/fs-list/workspace/README.txt | 1 - .../fs-list/workspace/docs/guide.txt | 1 - .../snapshots/fs-list/workspace/package.json | 1 - .../snapshots/fs-list/workspace/src/index.txt | 1 - .../lsp-definition/system-prompt.expected.md | 2 - .../lsp-definition/tool-schemas.expected.json | 17 - .../pty-tools/system-prompt.expected.md | 2 - .../pty-tools/tool-schemas.expected.json | 17 - .../system-prompt.expected.md | 2 - .../tool-schemas.expected.json | 17 - .../skill-load/system-prompt.expected.md | 2 - .../skill-load/tool-schemas.expected.json | 17 - .../text-turn/system-prompt.expected.md | 2 - .../text-turn/tool-schemas.expected.json | 17 - .../web-fetch/system-prompt.expected.md | 2 - .../web-fetch/tool-schemas.expected.json | 17 - .../system-prompt.expected.md | 2 - .../tool-schemas.expected.json | 17 - .../core/tools/tests/gen-tool-catalog.spec.ts | 2 +- packages/fs/tool-fs/README.i18n.yaml | 6 +- packages/fs/tool-fs/README.md | 45 +- packages/fs/tool-fs/README.zh.md | 45 +- packages/fs/tool-fs/package.json | 2 +- packages/fs/tool-fs/src/index.ts | 18 +- packages/fs/tool-fs/src/list-render.ts | 137 ----- packages/fs/tool-fs/src/list.ts | 150 ----- packages/fs/tool-fs/tests/list-render.spec.ts | 79 --- packages/fs/tool-fs/tests/tools.spec.ts | 172 +----- scripts/gen-tool-catalog.ts | 4 +- 55 files changed, 775 insertions(+), 1171 deletions(-) rename .agents/notes/implemented/{feature/2026-07-27-directory-listing-tool.i18n.yaml => bug-fix/2026-07-27-glob-sampling.i18n.yaml} (58%) create mode 100644 .agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.md create mode 100644 .agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md delete mode 100644 .agents/notes/implemented/feature/2026-07-27-directory-listing-tool.md delete mode 100644 .agents/notes/implemented/feature/2026-07-27-directory-listing-tool.zh.md create mode 100644 examples/acp-agent/fs-search.cordis.snapshot.yml create mode 100644 examples/acp-agent/fs-search.cordis.yml create mode 100755 examples/acp-agent/tests/fixtures/fs-search-bin/rg create mode 100644 examples/acp-agent/tests/snapshots/fs-glob-sampling/input.json create mode 100644 examples/acp-agent/tests/snapshots/fs-glob-sampling/session.jsonl rename examples/acp-agent/tests/snapshots/{fs-list => fs-glob-sampling}/stdout.expected.jsonl (90%) create mode 100644 examples/acp-agent/tests/snapshots/fs-glob-sampling/system-prompt.expected.md create mode 100644 examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/input.json delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/session.jsonl delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/workspace/README.txt delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/workspace/docs/guide.txt delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/workspace/package.json delete mode 100644 examples/acp-agent/tests/snapshots/fs-list/workspace/src/index.txt delete mode 100644 packages/fs/tool-fs/src/list-render.ts delete mode 100644 packages/fs/tool-fs/src/list.ts delete mode 100644 packages/fs/tool-fs/tests/list-render.spec.ts diff --git a/.agents/notes/implemented/feature/2026-07-27-directory-listing-tool.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.i18n.yaml similarity index 58% rename from .agents/notes/implemented/feature/2026-07-27-directory-listing-tool.i18n.yaml rename to .agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.i18n.yaml index 9bf1c8cb73..88e4ec9fc8 100644 --- a/.agents/notes/implemented/feature/2026-07-27-directory-listing-tool.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.i18n.yaml @@ -1,6 +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/feature/2026-07-27-directory-listing-tool.md -2026-07-27-directory-listing-tool.md: fd8e8fb22d0b75a4d457e800296fa3ce2c2b5bd5 -2026-07-27-directory-listing-tool.zh.md: 7d4e797878858ea1ca09f1eea6c6a3953c9abae6 +# 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 diff --git a/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.md b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.md new file mode 100644 index 0000000000..c583f0cd81 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.md @@ -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. diff --git a/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md new file mode 100644 index 0000000000..97025a7714 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md @@ -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` ACP(Agent Client Protocol)场景会启动真实的 Loader/app/sandbox-bash 组合,并让真实搜索插件对接确定性的 `rg` 进程 fixture(测试前置数据);其结果覆盖 4 个顶层条目,而不是只返回某棵子树的前部。 diff --git a/.agents/notes/implemented/feature/2026-07-27-directory-listing-tool.md b/.agents/notes/implemented/feature/2026-07-27-directory-listing-tool.md deleted file mode 100644 index fd8e8fb22d..0000000000 --- a/.agents/notes/implemented/feature/2026-07-27-directory-listing-tool.md +++ /dev/null @@ -1,109 +0,0 @@ -# Agent Note: Sample over-cap glob results across the tree, and give the model a directory-listing tool - -Status: implemented - -English | [中文](2026-07-27-directory-listing-tool.zh.md) - -## Problem - -Asked what a workspace contained, an agent described one subfolder as if it were the whole project. - -The session log shows exactly how. The workspace held 22 top-level entries and 11,485 files. The model called `glob {"pattern": "*"}`, which matched 10,030 paths; the tool showed the first 100, and all 100 sat under a single recently-unpacked subdirectory holding 355 of those files. The model never saw the other 21 top-level entries and answered from the one it did see. The session cwd was correct throughout — nothing was misconfigured, and every number the tool printed was true. - -Three properties of `glob` compose into that page: - -- **A pattern with no `/` matches at any depth.** The pattern goes to ripgrep as `--glob=`, where a glob without a separator matches the basename anywhere in the tree. `*` therefore means "every file in the workspace", not "the top level" — the opposite of what it means in a shell. The tool said nothing about this, and every example in its schema was `**/…`, so nothing suggested the plain form was recursive. -- **`--sort=modified` orders oldest first.** Unpacking an archive restores the timestamps stored inside it, which predate everything the user wrote, so a freshly unpacked subtree lands at the very front of any broad match. (Ripgrep sorts ascending; `--sortr` is the descending form and is not used here.) -- **The inline page was the head of that order.** `globMaxResults` (100) paths were kept from the front. Under the first two properties, one subtree takes every slot. - -Each property is defensible alone. Together they make the most ordinary request an agent receives — "what is in this directory" — reliably produce a confident wrong answer, because nothing in the result distinguishes "the 100 oldest files in this workspace" from "this workspace". - -### What ordering can and cannot fix - -A directory's name reaches the model only as the prefix of one of its files' paths, since `rg --files` emits files and never directory entries. That is enough for ordering to matter a great deal, and not enough for `glob` to answer the question. - -Measured on a reproduction of the failure's shape — 24 top-level entries, 716 files, one old-timestamped subtree: - -| First 100 paths chosen by | Distinct top-level names visible | -| --- | --- | -| modification time, oldest first (the shipped behavior) | 7 | -| round-robin across top-level entries | 21 | - -So a differently chosen page does surface most of the missing names, and the original diagnosis that ordering could not have helped was wrong. What no ordering fixes: an entry with no files beneath it never appears at all (the reproduction's empty directory is absent from the complete 716-path output), and nothing in the output says which names are directories or how many entries a directory holds. `glob` can therefore convey a tree's rough shape; it cannot state a directory's contents. - -## Decision - -Two changes, in the two packages that own the two halves of the failure. - -### The inline page of an over-cap `glob` result is sampled, not taken from the head - -`@deepseek-ai/dsh-tool-fs-search`. A result within `globMaxResults` is unchanged: shown whole, in modification-time order. Only when the result is larger does the page change — and there, taking the head is what fails. - -`sampleAcrossTopLevel` removes the displayed search-root prefix, groups the complete result by the next path segment, and fills the page round-robin: every entry immediately beneath the actual relative or absolute root gets a slot before any entry gets a second, and an entry that runs out of paths drops out so its remaining slots go to the rest. Sort order survives where it still carries meaning — groups are visited in the order ripgrep first emits them, and each group's own paths keep their relative order. The page is emitted grouped by entry rather than interleaved, so the breadth is legible at a glance. - -The footer states the basis, because a page that silently stopped being "the first N in modification-time order" would be a second, quieter version of the same lie: - -``` -(Showing 100 of 10030 paths, sampled across 22 of the 22 top-level entries this pattern matched -instead of taken in modification-time order. Full sorted result stored at: …) -``` - -When the page cannot reach every top-level entry — more entries than slots — the footer says so with the shown/total spread and tells the model to narrow `path`. A flat result, where every path is its own top-level entry, keeps the plain `(Showing k of n paths. …)` footer: there the round-robin *is* the sorted head, and naming a spread would only restate the counts. The spill artifact always holds the complete list in modification-time order, so the sorted view is never lost. - -The guidance and schema stop misleading in the same change: "not shell find or ls" becomes "not shell find"; both now state that a pattern without `/` matches basenames at any depth, that results are files and never directories, and that a fitting result is modification-time ordered while a larger one is sampled across top-level entries. They do not recommend sibling-package tools that may be absent from the current composition. - -### `list`, in `@deepseek-ai/dsh-tool-fs` - -A fourth model-facing filesystem tool over the existing `ctx.fs.listDir` primitive, which until now shipped with skill discovery as its only consumer; [the seam Agent Note](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md) deferred the model-facing consumer to a separate decision, which this is. - -It takes optional `path` and 1-based `offset` arguments, defaulting to the calling agent's session workspace and entry 1, and returns one bounded page as `{ path, offset, entries: [{ name, type }], totalEntries, counts }`, where `type` is `file`, `directory`, or `other`. It calls `listDir` and nothing else: the seam already reports absence as `FS_NOT_FOUND` and a non-directory target as `FS_NOT_DIRECTORY`, so a preceding `stat` would add a round-trip and a second source of truth. - -Three presentation rules carry the decision: - -- **Directories sort first, then files, then non-regular children, each alphabetically** before paging, so every offset traverses one stable order and the first page keeps navigable structure. -- **The canonical value and Native result carry one recoverable page** of at most `listMaxEntries` (default 200, configurable). The footer states the complete size and composition and gives `offset=` until the final page, so omitted sibling names remain reachable. -- **Filesystem text cannot forge presentation structure.** A name is emitted verbatim unless it would make the listing lie — a control character splitting one entry across lines, `` 交给 ripgrep,而不含分隔符的 glob 匹配树中任何位置的基名。因此 `*` 的含义是「工作区里的每个文件」,而不是「顶层」——与它在 shell 里的含义正好相反。工具对此只字未提,schema 里的示例又全是 `**/…`,没有任何线索表明朴素写法是递归的。 -- **`--sort=modified` 按从旧到新排序。** 解包压缩档会还原档案内部保存的时间戳,它们早于用户自己写下的一切,因此新近解包的子树会落在任何宽泛匹配的最前面。(ripgrep 按升序排序;降序是 `--sortr`,此处未使用。) -- **内联页面取的是该顺序的头部。** 从最前面保留 `globMaxResults`(100)条路径。在前两项性质之下,一个子树吃掉全部位置。 - -单看每一项都站得住。合在一起,它们让 agent 收到的最普通的请求——「这个目录里有什么」——稳定地产出一个笃定的错误答案,因为结果里没有任何信息能区分「本工作区按从旧到新顺序排在最前的 100 个文件」与「本工作区」。 - -### 排序能修什么,不能修什么 - -由于 `rg --files` 输出文件、从不输出目录条目,一个目录的名字只能作为其下某个文件路径的前缀抵达模型。这既足以让排序变得非常重要,也不足以让 `glob` 回答那个问题。 - -在一份复现该失败形态的目录上实测——24 个顶层条目、716 个文件、一个时间戳较旧的子树: - -| 前 100 条路径的挑选方式 | 可见的顶层名个数 | -| --- | --- | -| 按修改时间、从旧到新(已交付的行为) | 7 | -| 跨顶层条目轮转 | 21 | - -也就是说,换一种页面挑选方式确实能呈现出大部分缺失的名字,最初那句「排序帮不上忙」的诊断是错的。排序修不了的是:没有任何文件的条目根本不会出现(复现目录里的空目录在完整的 716 条输出中一次都没出现),而且输出里没有任何信息说明哪些名字是目录、某个目录有多少条目。因此 `glob` 能传达一棵树的大致形状,却说不出一个目录的内容。 - -## Decision - -两项改动,分别落在承担这次失败两半责任的两个包中。 - -### 超过上限的 `glob` 结果,内联页面改为取样而非取头部 - -`@deepseek-ai/dsh-tool-fs-search`。未超过 `globMaxResults` 的结果完全不变:整体展示,按修改时间排序。只有结果更大时页面才改变——而恰恰在这种情况下,取头部是会失败的做法。 - -`sampleAcrossTopLevel` 移除所显示的搜索根前缀,再按下一个路径段对完整结果分组,并以轮转方式填充页面:实际相对或绝对搜索根正下方的每个条目都先拿到一个位置,任何条目才可能拿到第二个;某个条目的路径用尽后退出,其剩余份额交给其余条目。排序在仍有意义的地方被保留下来——分组按 ripgrep 首次输出它们的顺序访问,而每个分组内部的路径保持原有相对顺序。页面按条目分组输出而非交错输出,使覆盖广度一眼可辨。 - -footer 会说明取用依据,因为一个悄悄不再是「按修改时间排序的前 N 条」的页面,只会成为同一个谎言更安静的版本: - -``` -(Showing 100 of 10030 paths, sampled across 22 of the 22 top-level entries this pattern matched -instead of taken in modification-time order. Full sorted result stored at: …) -``` - -当页面无法触达全部顶层条目时——条目数多于位置数——footer 会给出已触达/总计的分布,并要求模型缩小 `path`。扁平结果(每个路径各自构成一个顶层条目)保留朴素的 `(Showing k of n paths. …)` footer:此时轮转结果**就是**排序后的头部,说明分布只会重复计数。spill 产物始终保存按修改时间排序的完整列表,排序视图不会丢失。 - -同一次改动里,指导与 schema 也不再误导:「not shell find or ls」改为「not shell find」;两处现在都写明:不含 `/` 的 pattern 匹配任意深度的基名、结果只有文件从不包含目录、未超上限的结果按修改时间排序,而更大的结果跨顶层条目取样。它们不会推荐当前组合中可能不存在的兄弟包工具。 - -### `list`,位于 `@deepseek-ai/dsh-tool-fs` - -在既有的 `ctx.fs.listDir` 原语之上新增第四个面向模型的文件系统工具;该原语此前交付时唯一的消费方是 skill(技能)发现,[seam Agent Note](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md) 把面向模型的消费方推迟为一项单独的决策,也就是本文。 - -它接受可选的 `path` 和从 1 开始的 `offset` 参数,默认取调用 agent 的会话工作区和第 1 个条目,并返回一个有界页面,形如 `{ path, offset, entries: [{ name, type }], totalEntries, counts }`,其中 `type` 为 `file`、`directory` 或 `other`。它只调用 `listDir`:seam 已经把不存在报告为 `FS_NOT_FOUND`、把非目录目标报告为 `FS_NOT_DIRECTORY`,前置 `stat` 只会多一次往返并制造第二个真源。 - -有三条展示规则承载了这个决策: - -- **先目录、再文件、最后非常规子项,各组内按字母序**,然后再分页,使每个 offset 都遍历同一稳定顺序,且第一页保留可导航的结构。 -- **规范值和 Native 结果携带一个可继续取回的页面**,最多包含 `listMaxEntries` 个条目(默认 200,可配置)。footer 会说明完整规模与构成,并在最后一页之前给出 `offset=`,因此被省略的同级名称仍可取回。 -- **文件系统文本无法伪造展示结构。** 条目名默认原样输出;只有当它会让列出结果失真时才转为 JSON 字符串并中和 `; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal: Record; - /** List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents. */ - list: { - /** Directory to list. Defaults to the session workspace; a relative path resolves against it. */ - path?: string; - /** 1-based first entry to return. Defaults to 1; use the footer value to continue. */ - offset?: number; - } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -307,20 +298,6 @@ interface ToolOutputMap { }; activation: "armed" | "disarmed"; }; - list: { - path: string; - offset: number; - entries: ({ - name: string; - type: "file" | "directory" | "other"; - })[]; - totalEntries: number; - counts: { - directories: number; - files: number; - other: number; - }; - }; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json index 93457f0e37..314b24e2be 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json @@ -172,23 +172,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md index 1a8589a47b..0cee2a6517 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. @@ -82,13 +80,6 @@ interface ToolArgsMap { } & Record; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal: Record; - /** List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents. */ - list: { - /** Directory to list. Defaults to the session workspace; a relative path resolves against it. */ - path?: string; - /** 1-based first entry to return. Defaults to 1; use the footer value to continue. */ - offset?: number; - } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -278,20 +269,6 @@ interface ToolOutputMap { }; activation: "armed" | "disarmed"; }; - list: { - path: string; - offset: number; - entries: ({ - name: string; - type: "file" | "directory" | "other"; - })[]; - totalEntries: number; - counts: { - directories: number; - files: number; - other: number; - }; - }; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json index 9f55d9291f..b61d7bf623 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md index 1a8589a47b..0cee2a6517 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. @@ -82,13 +80,6 @@ interface ToolArgsMap { } & Record; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal: Record; - /** List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents. */ - list: { - /** Directory to list. Defaults to the session workspace; a relative path resolves against it. */ - path?: string; - /** 1-based first entry to return. Defaults to 1; use the footer value to continue. */ - offset?: number; - } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -278,20 +269,6 @@ interface ToolOutputMap { }; activation: "armed" | "disarmed"; }; - list: { - path: string; - offset: number; - entries: ({ - name: string; - type: "file" | "directory" | "other"; - })[]; - totalEntries: number; - counts: { - directories: number; - files: number; - other: number; - }; - }; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md index 1a8589a47b..0cee2a6517 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. @@ -82,13 +80,6 @@ interface ToolArgsMap { } & Record; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal: Record; - /** List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents. */ - list: { - /** Directory to list. Defaults to the session workspace; a relative path resolves against it. */ - path?: string; - /** 1-based first entry to return. Defaults to 1; use the footer value to continue. */ - offset?: number; - } & Record; /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */ ralph: { /** The immutable completion objective for every fresh Ralph round. */ @@ -278,20 +269,6 @@ interface ToolOutputMap { }; activation: "armed" | "disarmed"; }; - list: { - path: string; - offset: number; - entries: ({ - name: string; - type: "file" | "directory" | "other"; - })[]; - totalEntries: number; - counts: { - directories: number; - files: number; - other: number; - }; - }; ralph: { runId: string; agentsStarted: number; diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/escalation-approved/system-prompt.expected.md index cd3c81f6f8..e3437ad61a 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/escalation-approved/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/escalation-approved/tool-schemas.expected.json index 7094daf44e..01ac777a42 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/escalation-approved/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/fs-glob-sampling/input.json b/examples/acp-agent/tests/snapshots/fs-glob-sampling/input.json new file mode 100644 index 0000000000..cc5fc95e59 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/input.json @@ -0,0 +1,7 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "prompt", "text": "Call glob exactly once with pattern * and no path. Then reply with exactly GLOB_SAMPLED and nothing else." } + ] +} diff --git a/examples/acp-agent/tests/snapshots/fs-glob-sampling/session.jsonl b/examples/acp-agent/tests/snapshots/fs-glob-sampling/session.jsonl new file mode 100644 index 0000000000..16a7d9b631 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/session.jsonl @@ -0,0 +1,24 @@ +{"type":"session","version":0,"id":"f5a99d52-3eaa-4ce7-858d-61d4fd77df2a","createdAt":1785218400000,"cwd":"/tmp/acp-fs-glob-sampling","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1785218400001,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1785218400002,"data":{"content":[{"type":"text","text":"Call glob exactly once with pattern * and no path. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1785218400003,"data":{"title":"Call glob exactly once with","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":3,"time":1785218400004,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":4,"time":1785218400005,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":5,"time":1785218400006,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":6,"time":1785218400007,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"glob-sampling-call","name":"glob","argumentsDelta":"{\"pattern\":\"*\"}"}}} +{"type":"assistant/chunk","seq":7,"time":1785218400008,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\"}"}}}} +{"type":"assistant/chunk","seq":8,"time":1785218400009,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1,"outputTokens":1}}}} +{"type":"assistant/chunk","seq":9,"time":1785218400010,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":10,"time":1785218400011,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-pro"},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} +{"type":"tool/call","seq":11,"time":1785218400012,"data":{"turn":1,"step":1,"callId":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\"}"}} +{"type":"tool/result","seq":12,"time":1785218400013,"data":{"turn":1,"step":1,"callId":"glob-sampling-call","content":[{"type":"text","text":"archive/a.ts\nsrc/index.ts\ndocs/guide.md\ntest/spec.ts\n\n(Showing 4 of 6 paths, sampled across 4 of the 4 top-level entries this pattern matched instead of taken in modification-time order. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"step/end","seq":13,"time":1785218400014,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":14,"time":1785218400015,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":15,"time":1785218400016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":16,"time":1785218400017,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"GLOB_SAMPLED"}}} +{"type":"assistant/chunk","seq":17,"time":1785218400018,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GLOB_SAMPLED"}}}} +{"type":"assistant/chunk","seq":18,"time":1785218400019,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":1,"outputTokens":1}}}} +{"type":"assistant/chunk","seq":19,"time":1785218400020,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":20,"time":1785218400021,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"GLOB_SAMPLED"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-pro"},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"} +{"type":"step/end","seq":21,"time":1785218400022,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":22,"time":1785218400023,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-list/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-glob-sampling/stdout.expected.jsonl similarity index 90% rename from examples/acp-agent/tests/snapshots/fs-list/stdout.expected.jsonl rename to examples/acp-agent/tests/snapshots/fs-glob-sampling/stdout.expected.jsonl index 22d36a3c79..691b11cef0 100644 --- a/examples/acp-agent/tests/snapshots/fs-list/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/stdout.expected.jsonl @@ -1,4 +1,4 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} {"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"docs src"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"GLOB_SAMPLED"}}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/fs-glob-sampling/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/fs-glob-sampling/system-prompt.expected.md new file mode 100644 index 0000000000..467b05904b --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/system-prompt.expected.md @@ -0,0 +1,29 @@ +You are an AI agent powered by the DeepSeek Harness SDK. + +You are a coding assistant powered by the deepseek-v4-pro model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. + +Verify your work by running the code or tests. Keep answers brief and factual. + + +Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. + +Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. + +Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session. + +Use the glob tool — not shell find — to discover files by path pattern. A pattern with no "/" matches basenames at any depth, so "*" matches every file in the tree rather than its top level. Results are files only, never directories, and include hidden and ignored files: a result that fits comes back in modification-time order, while a larger one is sampled across top-level entries, so it spans the tree instead of one subtree. + +Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context. + +Check the [exit code: N] marker on every bash result; investigate failures before moving on. + +Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering. + +Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked. + +Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`). + + +Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. + +Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out. diff --git a/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json new file mode 100644 index 0000000000..1b25291c5c --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-glob-sampling/tool-schemas.expected.json @@ -0,0 +1,517 @@ +{ + "initial": [ + { + "name": "bash", + "description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to execute." + }, + "description": { + "type": "string", + "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"." + }, + "timeoutMs": { + "type": "number", + "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry." + }, + "workdir": { + "type": "string", + "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it." + }, + "run_in_background": { + "type": "boolean", + "description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access." + } + }, + "required": [ + "command", + "description" + ] + } + }, + { + "name": "create_goal", + "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.", + "parameters": { + "type": "object", + "properties": { + "objective": { + "type": "string", + "description": "The concrete completion objective inferred from the direct human request." + }, + "max_goal_rounds": { + "type": "number", + "description": "Optional positive safe-integer limit on automatic continuation rounds." + } + }, + "required": [ + "objective" + ] + } + }, + { + "name": "edit", + "description": "Edit an existing UTF-8 text file by replacing literal text.", + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to edit, resolved by the filesystem backend." + }, + "old_string": { + "type": "string", + "description": "Literal text to replace. Must match exactly." + }, + "new_string": { + "type": "string", + "description": "Literal replacement text. Use an empty string to delete the match." + }, + "replace_all": { + "type": "boolean", + "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "old_string", + "new_string" + ] + } + }, + { + "name": "get_goal", + "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "name": "glob", + "description": "Find files whose paths match a glob pattern. Returns matching file paths — never directories — including hidden and ignored files (VCS metadata directories are excluded). Up to 4 paths come back in modification-time order; a larger result instead returns 4 paths sampled across top-level entries, says so, and reports where the complete sorted list was saved. This tool does not enumerate directory entries.", + "parameters": { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Glob pattern to match file paths against (e.g. \"**/*.ts\", \"src/**/*.test.js\"). A pattern with no \"/\" matches the basename at any depth, so \"*\" and \"*.ts\" both search the whole tree; include a separator to anchor the depth." + }, + "path": { + "type": "string", + "description": "Directory to search in. Defaults to the session workspace; a relative path resolves against it." + } + }, + "required": [ + "pattern" + ] + } + }, + { + "name": "grep", + "description": "Search file contents with a ripgrep regular expression. Returns matching lines with line numbers, grouped by file. Returns the first 250 matches inline; a capped result reports where the complete match list was saved. Use read on a matched file for surrounding context.", + "parameters": { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression to search for (ripgrep syntax)." + }, + "path": { + "type": "string", + "description": "File or directory to search. Defaults to the session workspace; a relative path resolves against it." + }, + "include": { + "type": "string", + "description": "One glob filter for which files to search (e.g. \"*.ts\", \"*.{js,jsx}\"). Not a list; negation is not supported." + } + }, + "required": [ + "pattern" + ] + } + }, + { + "name": "ralph", + "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", + "parameters": { + "type": "object", + "properties": { + "objective": { + "type": "string", + "description": "The immutable completion objective for every fresh Ralph round." + }, + "maxRounds": { + "type": "number", + "description": "Optional positive safe-integer round cap, bounded by the deployment ceiling." + } + }, + "required": [ + "objective" + ] + } + }, + { + "name": "read", + "description": "Read a UTF-8 text file and return line-numbered content.", + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to read, resolved by the filesystem backend." + }, + "offset": { + "type": "number", + "description": "1-based first line to return. Defaults to 1." + }, + "limit": { + "type": "number", + "description": "Maximum number of lines to return. Defaults to 2000." + } + }, + "required": [ + "file_path" + ] + } + }, + { + "name": "skill", + "description": "Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact skill name from the available skills list." + } + }, + "required": [ + "name" + ] + } + }, + { + "name": "subagent", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.", + "parameters": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "A short (3-5 word) description of the delegated task, for display." + }, + "prompt": { + "type": "string", + "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." + }, + "run_in_background": { + "type": "boolean", + "description": "Run as a background task and return its id; collect with task_output or stop with task_kill." + } + }, + "required": [ + "description", + "prompt" + ] + } + }, + { + "name": "subagent_fork", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.", + "parameters": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "A short (3-5 word) description of the delegated task, for display." + }, + "prompt": { + "type": "string", + "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "run_in_background": { + "type": "boolean", + "description": "Run as a background task and return its id; collect with task_output or stop with task_kill." + } + }, + "required": [ + "description", + "prompt" + ] + } + }, + { + "name": "task_kill", + "description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.", + "parameters": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "Task id returned by the tool that started the background work." + }, + "reason": { + "type": "string", + "description": "Optional short reason, recorded in the log and forwarded to the task." + } + }, + "required": [ + "task_id" + ] + } + }, + { + "name": "task_list", + "description": "List your background tasks (running and finished) with their ids, kinds, and statuses.", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "name": "task_output", + "description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.", + "parameters": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "Task id returned by the tool that started the background work." + }, + "wait": { + "type": "boolean", + "description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive." + }, + "timeout_ms": { + "type": "number", + "description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum." + } + }, + "required": [ + "task_id" + ] + } + }, + { + "name": "todo_write", + "description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).", + "parameters": { + "type": "object", + "properties": { + "todos": { + "type": "array", + "description": "The COMPLETE task list, replacing any previous list.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "content": { + "type": "string", + "description": "What the task is — a short imperative line." + }, + "status": { + "type": "string", + "description": "pending (not started) | in_progress (now) | completed (done).", + "enum": [ + "pending", + "in_progress", + "completed" + ] + } + }, + "required": [ + "content", + "status" + ] + } + } + }, + "required": [ + "todos" + ] + } + }, + { + "name": "update_goal", + "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.", + "parameters": { + "type": "object", + "properties": { + "goal_id": { + "type": "string", + "description": "Exact id returned by get_goal." + }, + "revision": { + "type": "number", + "description": "Exact positive revision returned by get_goal." + }, + "action": { + "type": "string", + "description": "edit | pause | resume | complete | blocked", + "enum": [ + "edit", + "pause", + "resume", + "complete", + "blocked" + ] + }, + "objective": { + "type": "string", + "description": "Replacement objective; valid only with action edit." + }, + "max_goal_rounds": { + "type": "number", + "description": "Replacement cap; valid only with action edit." + }, + "blocked_reason": { + "type": "string", + "description": "Concrete blocking condition; required only with action blocked." + } + }, + "required": [ + "goal_id", + "revision", + "action" + ] + } + }, + { + "name": "workflow", + "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.", + "parameters": { + "type": "object", + "properties": { + "script": { + "type": "string", + "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)." + }, + "meta": { + "type": "object", + "description": "The workflow identity block (plain JSON — never code).", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "Short kebab-case workflow name." + }, + "description": { + "type": "string", + "description": "One-line description of what the workflow does." + }, + "whenToUse": { + "type": "string", + "description": "Optional guidance on when this workflow applies." + }, + "phases": { + "type": "array", + "description": "Optional phase declarations matched by phase() calls.", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "title": { + "type": "string", + "description": "The phase title phase() calls match by exact string." + }, + "detail": { + "type": "string", + "description": "Optional one-line description of the phase." + }, + "provider": { + "type": "string", + "description": "Optional provider override this phase is expected to use." + }, + "model": { + "type": "string", + "description": "Optional model override this phase is expected to use." + } + }, + "required": [ + "title" + ] + } + } + }, + "required": [ + "name", + "description" + ] + }, + "args": { + "type": "object", + "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).", + "additionalProperties": true + } + }, + "required": [ + "script", + "meta" + ] + } + }, + { + "name": "write", + "description": "Create or fully replace a UTF-8 text file.", + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to write, resolved by the filesystem backend." + }, + "content": { + "type": "string", + "description": "Full UTF-8 text content to write." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "content" + ] + } + } + ], + "changes": [] +} diff --git a/examples/acp-agent/tests/snapshots/fs-list/input.json b/examples/acp-agent/tests/snapshots/fs-list/input.json deleted file mode 100644 index 67a0bc3f42..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/input.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "steps": [ - { "op": "initialize" }, - { "op": "newSession" }, - { "op": "prompt", "text": "Call the list tool exactly once, with no arguments at all (NOT bash, NOT glob, and do not pass a path). Then reply with exactly the names of the subdirectories it reported, alphabetically, separated by a single space, and nothing else." } - ] -} diff --git a/examples/acp-agent/tests/snapshots/fs-list/session.jsonl b/examples/acp-agent/tests/snapshots/fs-list/session.jsonl deleted file mode 100644 index 0a05e2b1fe..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/session.jsonl +++ /dev/null @@ -1,32 +0,0 @@ -{"type":"session","version":0,"id":"ea640046-d959-4644-9a03-ff3d08133d4b","createdAt":1785159113359,"cwd":"/var/folders/8k/kj35k1fd6t90n0czg7k3hv140000gn/T/acp-snap-cwd-QJ2ZC6","delegationDepth":0} -{"type":"turn/start","seq":0,"time":1785159113361,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":1785159113362,"data":{"content":[{"type":"text","text":"Call the list tool exactly once, with no arguments at all (NOT bash, NOT glob, and do not pass a path). Then reply with exactly the names of the subdirectories it reported, alphabetically, separated by a single space, and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":2,"time":1785159113363,"data":{"title":"Call the list tool exactly","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":3,"time":1785159113368,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":1785159113369,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":5,"time":1785159115248,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"reasoning-chunks","seq0":6,"time0":1785159115249,"data":{"turn":1,"step":1,"index":0,"dt":[146,45,3,0,0,1,43,1,0,0,1,1,31,1,46,0,0,0,0,0,41,1,0,0,45,2,2,0,0,1,44,3,3,0],"texts":["The"," user"," wants"," me"," to"," call"," the"," list"," tool"," with"," no"," arguments"," and"," then"," reply"," with"," the"," names"," of"," the"," sub","direct","ories"," it"," reports",","," alphabet","ically",","," separated"," by"," a"," single"," space","."]}} -{"type":"assistant/chunk","seq":41,"time":1785159115836,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":42,"time":1785159115837,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_BSi06gcYvl6oad3I5qqG9244","name":"list","argumentsDelta":""}}} -{"type":"assistant/chunk","seq":43,"time":1785159115837,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_BSi06gcYvl6oad3I5qqG9244","name":"list","argumentsDelta":"{}"}}} -{"type":"assistant/chunk","seq":44,"time":1785159115921,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call the list tool with no arguments and then reply with the names of the subdirectories it reports, alphabetically, separated by a single space."}}}} -{"type":"assistant/chunk","seq":45,"time":1785159115921,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_BSi06gcYvl6oad3I5qqG9244","name":"list","arguments":"{}"}}}} -{"type":"assistant/chunk","seq":46,"time":1785159115921,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":5513,"outputTokens":62,"cacheReadTokens":0,"reasoningTokens":35}}}} -{"type":"assistant/chunk","seq":47,"time":1785159115922,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":48,"time":1785159115926,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to call the list tool with no arguments and then reply with the names of the subdirectories it reports, alphabetically, separated by a single space."},{"type":"tool-call","id":"call_00_BSi06gcYvl6oad3I5qqG9244","name":"list","arguments":"{}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":5513,"outputTokens":62,"cacheReadTokens":0,"reasoningTokens":35}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"surfaceOp":"append"} -{"type":"tool/call","seq":49,"time":1785159115927,"data":{"turn":1,"step":1,"callId":"call_00_BSi06gcYvl6oad3I5qqG9244","name":"list","arguments":"{}"}} -{"type":"tool/result","seq":50,"time":1785159115944,"data":{"turn":1,"step":1,"callId":"call_00_BSi06gcYvl6oad3I5qqG9244","content":[{"type":"text","text":"/var/folders/8k/kj35k1fd6t90n0czg7k3hv140000gn/T/acp-snap-cwd-QJ2ZC6\ndirectory\n\ndocs/\nsrc/\npackage.json\nREADME.txt\n\n(4 entries: 2 directories, 2 files)\n"}],"isError":false},"sourceEventSeqs":[49],"surfaceOp":"append"} -{"type":"step/end","seq":51,"time":1785159115950,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":52,"time":1785159115951,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":53,"time":1785159116889,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"reasoning-chunks","seq0":54,"time0":1785159116890,"data":{"turn":1,"step":2,"index":0,"dt":[145,49,1,0,60,0,0,1,0,39,1,0,0,0,0,23,1,46,3,0,0],"texts":["The"," sub","direct","ories"," reported"," are"," `","docs","/","`"," and"," `","src","/","`."," Alphabet","ically",":"," docs",","," src","."]}} -{"type":"assistant/chunk","seq":76,"time":1785159117259,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":77,"time":1785159117259,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"docs"}}} -{"type":"assistant/chunk","seq":78,"time":1785159117297,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" src"}}} -{"type":"assistant/chunk","seq":79,"time":1785159117300,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subdirectories reported are `docs/` and `src/`. Alphabetically: docs, src."}}}} -{"type":"assistant/chunk","seq":80,"time":1785159117300,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"docs src"}}}} -{"type":"assistant/chunk","seq":81,"time":1785159117300,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":161,"outputTokens":25,"cacheReadTokens":5504,"reasoningTokens":22}}}} -{"type":"assistant/chunk","seq":82,"time":1785159117300,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":83,"time":1785159117301,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The subdirectories reported are `docs/` and `src/`. Alphabetically: docs, src."},{"type":"text","text":"docs src"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":161,"outputTokens":25,"cacheReadTokens":5504,"reasoningTokens":22}},"sourceEventSeqs":[53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82],"surfaceOp":"append"} -{"type":"step/end","seq":84,"time":1785159117308,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":85,"time":1785159117309,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-list/workspace/README.txt b/examples/acp-agent/tests/snapshots/fs-list/workspace/README.txt deleted file mode 100644 index dab306f45e..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/workspace/README.txt +++ /dev/null @@ -1 +0,0 @@ -# Project diff --git a/examples/acp-agent/tests/snapshots/fs-list/workspace/docs/guide.txt b/examples/acp-agent/tests/snapshots/fs-list/workspace/docs/guide.txt deleted file mode 100644 index 8c0d02fadc..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/workspace/docs/guide.txt +++ /dev/null @@ -1 +0,0 @@ -# Guide diff --git a/examples/acp-agent/tests/snapshots/fs-list/workspace/package.json b/examples/acp-agent/tests/snapshots/fs-list/workspace/package.json deleted file mode 100644 index e36fa754cf..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/workspace/package.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "demo" } diff --git a/examples/acp-agent/tests/snapshots/fs-list/workspace/src/index.txt b/examples/acp-agent/tests/snapshots/fs-list/workspace/src/index.txt deleted file mode 100644 index eab39ce89c..0000000000 --- a/examples/acp-agent/tests/snapshots/fs-list/workspace/src/index.txt +++ /dev/null @@ -1 +0,0 @@ -export const answer = 42 diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/lsp-definition/system-prompt.expected.md index d02b6d0859..7bde8fe289 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/lsp-definition/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-pro model. Your working di Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json index ab1bda91f3..9b5925605c 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "lsp", "description": "Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.", diff --git a/examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md index c5496aeed5..df065a83cb 100644 --- a/examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/pty-tools/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-pro model. Your working di Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json index 0d20d22762..8e093db8bd 100644 --- a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/session-query-spill/system-prompt.expected.md index 7d13160613..68bdd841c7 100644 --- a/examples/acp-agent/tests/snapshots/session-query-spill/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/session-query-spill/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json index caee8e049e..beb93c6b53 100644 --- a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md index e6ab6361db..17e6773a03 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json index 7094daf44e..01ac777a42 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md index e6ab6361db..17e6773a03 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json index 7094daf44e..01ac777a42 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/web-fetch/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/web-fetch/system-prompt.expected.md index 451746f412..45705db0a5 100644 --- a/examples/acp-agent/tests/snapshots/web-fetch/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/web-fetch/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-pro model. Your working di Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json index 6c389246e2..70940f8907 100644 --- a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/examples/acp-agent/tests/snapshots/workspace-context/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/workspace-context/system-prompt.expected.md index fdacca4b7f..6cd8d5725f 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/workspace-context/system-prompt.expected.md @@ -5,8 +5,6 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working Verify your work by running the code or tests. Keep answers brief and factual. -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. - Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files. Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes. diff --git a/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json index 7094daf44e..01ac777a42 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json @@ -115,23 +115,6 @@ "properties": {} } }, - { - "name": "list", - "description": "List the direct children of one directory, with their type. Entries are directories first, then files, each alphabetical; up to 200 are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. It includes subdirectories and is the tool for seeing one directory's contents.", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Directory to list. Defaults to the session workspace; a relative path resolves against it." - }, - "offset": { - "type": "number", - "description": "1-based first entry to return. Defaults to 1; use the footer value to continue." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 974d88c18c..3754595f56 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { it('boots every shipped tool package and harvests its model-facing schemas', async () => { const catalog = await collectToolCatalog() const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort() - expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'list', 'lsp', 'ralph', 'read', 'run_code', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write']) + expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write']) // Every tool carries a JSON-Schema `parameters` object (what the model sees). for (const entry of catalog) { for (const schema of entry.schemas) { diff --git a/packages/fs/tool-fs/README.i18n.yaml b/packages/fs/tool-fs/README.i18n.yaml index 3e7c989373..13f1ecd649 100644 --- a/packages/fs/tool-fs/README.i18n.yaml +++ b/packages/fs/tool-fs/README.i18n.yaml @@ -1,6 +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 packages/fs/tool-fs/README.md -README.md: 8fdd54fb36353ca6c1dbcf71cdad38c4bcf26b7d -README.zh.md: 567c8e0df58950369c59785859948e32cbccdef4 +# pnpm run verify-translation-pairing --write +README.md: 4ff9b043525e8e7a0b59e3d91410951d88bb9a69 +README.zh.md: f94a903c9c37f7d45b7f8cebabe21082388bd041 diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 8fdd54fb36..4ff9b04352 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -2,24 +2,23 @@ English | [中文](README.zh.md) -The **model-facing filesystem tools** — `list`, `read`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, **listing order**, and result formatting. It reads/writes/edits through the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)) **directly** — it injects `fs` (plus `tools`/`systemPrompt`), **not** a policy service. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. +The **model-facing filesystem tools** — `read`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, and result formatting. It reads/writes/edits through the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)) **directly** — it injects `fs` (plus `tools`/`systemPrompt`), **not** a policy service. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. ```ts ignore-check // Default deployment: a ctx.fs provider, the policy plugin, then the tools. await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-policy (policy gate) -await ctx.plugin(ToolFs) // this package — registers list/read/write/edit +await ctx.plugin(ToolFs) // this package — registers read/write/edit ``` `@deepseek-ai/dsh-fs-policy` is **optional**: omit it and the tools run against the bare provider (unconditional write/overwrite/edit, no observed-state). A deployment that loads these tools is expected to also load it, so the behavior is read-before-write/edit. ## Config -All keys are optional; the defaults are the shipped listing and read caps. +All keys are optional; the defaults are the shipped read caps. | Key | Default | Meaning | |---|---|---| -| `listMaxEntries` | `200` | Maximum entries one `list` page returns; the footer reports complete size and composition plus a next offset when more remain. | | `readLimit` | `2000` | Default and maximum lines returned by one `read` call (the tool schema advertises it as the `limit` default). | | `readMaxLineLength` | `2000` | Characters kept per line before truncation (the suffix names the cap). | | `readMaxBytes` | `51200` | Byte cap on one `read` call's selected lines; overflow ends the window with a "capped" footer. | @@ -29,20 +28,18 @@ All keys are optional; the defaults are the shipped listing and read caps. | Tool | Arguments | Behavior | |---|---|---| -| `list` | `path?`, `offset?` | One page of direct children with their type, defaulting to the session workspace and entry 1. Ordered directories first, then files, then non-regular children, each alphabetical; when more remain, continue from the footer's next offset. | | `read` | `file_path`, `offset?`, `limit?` | Line-numbered UTF-8 content with a pagination footer. `offset` is 1-based; `limit` defaults to and caps at the configured `readLimit` (2000). | | `write` | `file_path`, `content` | Create or fully replace a file. With the policy plugin: overwriting an existing file requires a prior `read` at the unchanged version; creating a new file does not. Without it: unconditional. | | `edit` | `file_path`, non-empty `old_string`, `new_string`, `replace_all?` | Literal replacement; unique match required unless `replace_all` is true. With the policy plugin: requires a prior `read` (any window) and the file unchanged since. Without it: unconditional. | Field names are snake_case to match Claude Code and existing harness tool schemas. -Canonical successes are `list` → `{ path, offset, entries: [{ name, type }], totalEntries, counts: { directories, files, other } }`, `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. `list.entries` is one bounded page; `type` is `file`, `directory`, or `other`, while its totals describe the complete directory. Native renderers preserve the listing/read envelopes and mutation acknowledgements below. Write/edit derive replayable diff-card metadata from these values; the values themselves are execution-local and are not added to `tool/result`. +Canonical successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. Native renderers preserve the line-numbered read and mutation acknowledgements below. Write/edit derive replayable diff-card metadata from these values; the values themselves are execution-local and are not added to `tool/result`. ## The tool is the executor; policy is an event gate The tools do **not** inject a policy service or inspect any cache. Each tool resolves the path via `ctx.fs.resolve(path, { cwd, signal })` — passing the calling agent's session cwd (`exec.agent.session.header.cwd`) so a relative path resolves against the session's workspace, matching `dsh-tool-bash`, and forwarding tool cancellation through resolution (see [the per-session cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)) — then: -- **list** — one `ctx.fs.listDir`; the seam already answers absence with `FS_NOT_FOUND` and a non-directory target with `FS_NOT_DIRECTORY`, so no probe precedes it. No `fs/observed`: a listing reads no file content and must not satisfy the read-before-write gate. (0 stat.) - **read** — one `ctx.fs.stat` (type + size routing + version), then `readText`/`streamText`, then builds the line window, then emits `fs/observed` with a plain `ctx.emit`. (1 stat.) - **write** — `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.writeText(target, content, intent)`, then `fs/observed`. (0 stat.) - **edit** — `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.editText(target, edit, intent)`, then `fs/observed`. (0 stat.) @@ -53,9 +50,9 @@ The tool passes `exec` (the tool-execution context) as the opaque `actor` on eve `fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event. -`list` and `read` opt into concurrent scheduling — `list` mutates nothing at all, and `read`'s only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md). +`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md). -The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Pure presentation lives beside the executors and is independently unit-tested: read windowing and output formatting in `src/read-render.ts`, listing order and envelope in `src/list-render.ts` (both Cordis-free); `src/list.ts`/`read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. +The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. ## Model Experience @@ -63,13 +60,7 @@ The package root exports only the Cordis plugin contract (`name`, `inject`, `Con #### What the model sees -Every request in this plugin's registration scope receives the independently registered list, read, write, and edit guidance below. Scoped tool restrictions can hide schemas without removing these sections. - -##### List guidance - -```markdown -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. -``` +Every request in this plugin's registration scope receives the independently registered read, write, and edit guidance below. Scoped tool restrictions can hide schemas without removing these sections. ##### Read guidance @@ -101,7 +92,7 @@ Prefix-stable while the plugin scope and guidance text are unchanged. Tool restr #### What the model sees -The model sees the generated [`list`, `read`, `write`, and `edit` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs), with snake_case arguments. Scoped tool restrictions can remove any definition for one agent. +The model sees the generated [`read`, `write`, and `edit` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs), with snake_case arguments. Scoped tool restrictions can remove any definition for one agent. #### Token effect @@ -111,20 +102,6 @@ Fixed schema cost on every request in that tool view. Prefix-stable while the visible tool definitions and order are unchanged. Registration lifecycle or scoped restrictions may invalidate reuse from the first changed schema token. -### List result - -#### What the model sees - -A successful listing is ``, newline, `directory`, newline, ``, one line per page entry, a blank line, one footer, and ``. A directory carries a trailing `/`, a non-regular child a trailing `@`, and a regular file neither. A name is emitted verbatim unless it could make the listing say something untrue — a control character, a leading `"`, a backslash, ` entries: directories, files)` with optional `, other`, or `(Showing entries - of : . Use offset= to continue.)`; the final page omits the continuation sentence. Every page states the complete count and composition. - -#### Token effect - -Listing output and its canonical `entries` page are capped by `listMaxEntries`; the retained call and result are resent until compaction. - -#### KV Cache effect - -Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries. - ### Read result #### What the model sees @@ -157,7 +134,7 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -Failures are normalized as `Error: `. This package's stable validation and read messages are `file_path must be a non-empty string`, `path must be a non-empty string when given`, `offset must be a positive integer`, `limit must be less than or equal to `, `old_string must be a non-empty string`, `old_string and new_string must differ`, `cannot read "": not found`, `cannot read "": not a regular file`, `offset is out of range for "" ( entries)`, and the corresponding ` lines` read error; provider and policy templates are quoted in their package READMEs. +Failures are normalized as `Error: `. This package's stable validation and read messages are `file_path must be a non-empty string`, `limit must be less than or equal to `, `old_string must be a non-empty string`, `old_string and new_string must differ`, `cannot read "": not found`, `cannot read "": not a regular file`, and `offset is out of range for "" ( lines)`; provider and policy templates are quoted in their package READMEs. #### Token effect @@ -169,6 +146,6 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work -- **`list` reads one directory level** — recursion and per-directory child counts are absent; offset pagination traverses only the current directory's ordered direct children. +- **No model-facing directory listing ships** — `ctx.fs.listDir` serves provider code such as skill discovery, while the sibling [`dsh-tool-fs-search`](../tool-fs-search/) package supplies bash-backed `glob` and `grep` rather than extending the filesystem seam. - **`read` handles UTF-8 text files only** — binary-safe reads and PDF/image/multimodal content are deferred; a directory target is `FS_NOT_REGULAR_FILE`. -- **No timeout surface** — `list`/`read`/`write`/`edit` take no timeout argument and declare no `timeout-policy` budget; cancellation rides `exec.signal` only (the deliberate [fs-family stance](../README.md)). +- **No timeout surface** — `read`/`write`/`edit` take no timeout argument and declare no `timeout-policy` budget; cancellation rides `exec.signal` only (the deliberate [fs-family stance](../README.md)). diff --git a/packages/fs/tool-fs/README.zh.md b/packages/fs/tool-fs/README.zh.md index 567c8e0df5..f94a903c9c 100644 --- a/packages/fs/tool-fs/README.zh.md +++ b/packages/fs/tool-fs/README.zh.md @@ -2,24 +2,23 @@ [English](README.md) | 中文 -**面向模型的文件系统工具**(`list`、`read`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON schema、参数校验、提示词段、**读取窗口逻辑**、**列出顺序** 和结果格式化。它**直接** 通过 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑:注入 `fs`(以及 `tools`/`systemPrompt`),**不** 注入政策服务。新鲜度/观察政策由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。 +**面向模型的文件系统工具**(`read`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON schema、参数校验、提示词段、**读取窗口逻辑** 和结果格式化。它**直接** 通过 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑:注入 `fs`(以及 `tools`/`systemPrompt`),**不** 注入政策服务。新鲜度/观察政策由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。 ```ts ignore-check // Default deployment: a ctx.fs provider, the policy plugin, then the tools. await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-policy (policy gate) -await ctx.plugin(ToolFs) // this package — registers list/read/write/edit +await ctx.plugin(ToolFs) // this package — registers read/write/edit ``` `@deepseek-ai/dsh-fs-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供编辑前读取行为。 ## 配置 -所有键均为可选;默认值是随产品交付的列出与读取上限。 +所有键均为可选;默认值是随产品交付的读取上限。 | 键 | 默认值 | 含义 | |---|---|---| -| `listMaxEntries` | `200` | 单个 `list` 页面返回的最大条目数;footer 会报告完整规模与构成,并在仍有条目时给出下一 offset。 | | `readLimit` | `2000` | 一次 `read` 调用返回的默认和最大行数(工具 schema 将其声明为 `limit` 默认值)。 | | `readMaxLineLength` | `2000` | 每行截断前保留的字符数(后缀会说明上限)。 | | `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限;溢出时以「已达上限」footer 结束窗口。 | @@ -29,20 +28,18 @@ await ctx.plugin(ToolFs) // this package — re | 工具 | 参数 | 行为 | |---|---|---| -| `list` | `path?`、`offset?` | 单个目录的一页直接子项及其类型,默认取会话工作区并从第 1 个条目开始。顺序为先目录、再文件、最后非常规子项,各组内按字母序排列;仍有条目时按 footer 给出的下一 offset 继续。 | | `read` | `file_path`、`offset?`、`limit?` | 带行号的 UTF-8 内容和分页 footer。`offset` 从 1 开始;`limit` 默认为配置的 `readLimit`(2000),上限也为该值。 | | `write` | `file_path`、`content` | 创建文件或完整替换文件。有政策插件时:覆盖现有文件要求先在未变版本上执行 `read`;创建新文件不需要。没有插件时:无条件执行。 | | `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换;除非 `replace_all` 为 true,否则要求唯一匹配。有政策插件时:要求先执行 `read`(任何窗口),且文件此后未变。没有插件时:无条件执行。 | 字段名使用 snake_case,与 Claude Code 和现有 harness 工具 schema 一致。 -规范成功值分别为:`list` → `{ path, offset, entries: [{ name, type }], totalEntries, counts: { directories, files, other } }`,`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。`list.entries` 是一个有界页面;`type` 为 `file`、`directory` 或 `other`,而总计信息描述的是完整目录。Native 渲染器会保留下方的列出/读取包络和变更确认。写入/编辑从这些值派生可回放的 diff 卡片元数据;值本身仅用于执行,不会添加到 `tool/result`。 +规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。Native 渲染器会保留下方带行号的读取结果和变更确认。写入/编辑从这些值派生可回放的 diff 卡片元数据;值本身仅用于执行,不会添加到 `tool/result`。 ## 工具就是执行器;政策是事件门禁 工具**不** 注入政策服务,也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径;它会传入调用 agent(智能体)的会话 cwd(`exec.agent.session.header.cwd`),使相对路径以会话工作区为基准解析并与 `dsh-tool-bash` 一致,同时把工具取消转发到解析过程(见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md))。随后执行: -- **list**:一次 `ctx.fs.listDir`;seam 已经把不存在报告为 `FS_NOT_FOUND`、把非目录目标报告为 `FS_NOT_DIRECTORY`,因此前面不需要任何探测。不发出 `fs/observed`:列出不读取任何文件内容,也不得满足编辑前读取门禁。(0 次 stat。) - **read**:一次 `ctx.fs.stat`(用于类型、大小路由和版本),随后调用 `readText`/`streamText`,构建行窗口,再发出 `fs/observed`,使用普通 `ctx.emit`。(1 次 stat。) - **write**:调用 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.writeText(target, content, intent)`,再发出 `fs/observed`。(0 次 stat。) - **edit**:调用 `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.editText(target, edit, intent)`,再发出 `fs/observed`。(0 次 stat。) @@ -53,9 +50,9 @@ await ctx.plugin(ToolFs) // this package — re `fs/observed` 在读取/写入/编辑已经成功之后,通过普通 `ctx.emit` 发出。监听器的契约是同步且只有副作用的记录器(`@deepseek-ai/dsh-fs-policy` 使用 `WeakMap.set`);工具不保护这次发出,因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。 -`list` 与 `read` 都允许并发调度:`list` 完全不做任何变更,而 `read` 的唯一变更是同步版本记录器。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此记录器竞态会以拒绝方式关闭;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。 +`read` 允许并发调度,因为其唯一变更是同步版本记录器。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此记录器竞态会以拒绝方式关闭;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。 -包根目录只导出 Cordis 插件契约(`name`、`inject`、`Config` 和 `apply`)。纯展示逻辑与执行器并列存放并单独进行单元测试:读取窗口与输出格式化位于 `src/read-render.ts`,列出顺序与包络位于 `src/list-render.ts`(两者均不依赖 Cordis);`src/list.ts`/`read.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。 +包根目录只导出 Cordis 插件契约(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。 ## 模型体验 @@ -63,13 +60,7 @@ await ctx.plugin(ToolFs) // this package — re #### 模型看到的内容 -该插件注册作用域内的每个请求都会收到下方独立注册的 list、read、write 与 edit 指导。作用域工具限制可以隐藏 schema,而不移除这些段。 - -##### List 指导 - -```markdown -Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. When a result is capped, continue with the offset named in its footer. -``` +该插件注册作用域内的每个请求都会收到下方独立注册的 read、write 与 edit 指导。作用域工具限制可以隐藏 schema,而不移除这些段。 ##### Read 指导 @@ -101,7 +92,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces #### 模型看到的内容 -模型会看到已生成的 [`list`、`read`、`write` 和 `edit` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs),参数使用 snake_case。作用域工具限制可以为某个 agent 移除任一定义。 +模型会看到已生成的 [`read`、`write` 和 `edit` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs),参数使用 snake_case。作用域工具限制可以为某个 agent 移除任一定义。 #### Token 影响 @@ -111,20 +102,6 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces 只要可见工具定义和顺序不变,前缀就保持稳定。注册生命周期或作用域限制可能从首个变化的 schema token 开始使复用失效。 -### 列出结果 - -#### 模型看到的内容 - -成功列出结果为 ``、换行、`directory`、换行、``、页面中的每个条目一行、一个空行、一条 footer 和 ``。目录带尾部 `/`,非常规子项带尾部 `@`,常规文件两者都不带。条目名默认原样输出,只有当它可能让列出结果失真时才转为 JSON 字符串并中和 ` entries: directories, files)`(可选追加 `, other`),或 `(Showing entries - of : . Use offset= to continue.)`;最后一页省略继续提示。每一页都会说明完整计数与构成。 - -#### Token 影响 - -列出输出及其规范 `entries` 页面受 `listMaxEntries` 限制;保留的调用与结果会反复发送,直到上下文压缩。 - -#### KV Cache 影响 - -仅追加;新增可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。 - ### 读取结果 #### 模型看到的内容 @@ -157,7 +134,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces #### 模型看到的内容 -失败会规范化为 `Error: `。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`path must be a non-empty string when given`、`offset must be a positive integer`、`limit must be less than or equal to `、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "": not found`、`cannot read "": not a regular file`、`offset is out of range for "" ( entries)`,以及对应的 ` lines` 读取错误;提供方和政策模板在各自包的 README 中逐字列出。 +失败会规范化为 `Error: `。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to `、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "": not found`、`cannot read "": not a regular file` 和 `offset is out of range for "" ( lines)`;提供方和政策模板在各自包的 README 中逐字列出。 #### Token 影响 @@ -169,6 +146,6 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces ## 已知限制与延期工作 -- **`list` 只读取一层目录**:不提供递归和逐目录子项计数;offset 分页只遍历当前目录中按顺序排列的直接子项。 +- **未交付面向模型的目录列出工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 [`dsh-tool-fs-search`](../tool-fs-search/) 包则提供基于 bash 的 `glob` 与 `grep`,而不是扩展文件系统 seam。 - **`read` 只处理 UTF-8 文本文件**:二进制安全读取和 PDF/图像/多模态内容均延期处理;目录目标为 `FS_NOT_REGULAR_FILE`。 -- **没有超时接口**:`list`/`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见有意采用的 [fs 能力族立场](../README.md))。 +- **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见有意采用的 [fs 能力族立场](../README.md))。 diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index 936d125013..737f7ac26b 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-tool-fs", - "description": "Model-facing filesystem tools (list, read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", + "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", "version": "0.0.1", "private": true, "type": "module", diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index 7890d5084d..a4c96d606b 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -1,19 +1,16 @@ /** - * Model-facing list, read, write, and edit tools over `ctx.fs`. This package owns schemas, - * validation, read windows, listing order, formatting, and observation events, never a concrete - * provider. An optional event policy supplies mutation guards; without one the tools use - * unconditional provider calls. + * Model-facing read, write, and edit tools over `ctx.fs`. This package owns schemas, validation, + * read windows, formatting, and observation events, never a concrete provider. An optional + * event policy supplies mutation guards; without one the tools use unconditional provider calls. * @module @deepseek-ai/dsh-tool-fs */ import type { Context } from 'cordis' import z from 'schemastery' import type {} from '@deepseek-ai/dsh-user-approval' -import { applyListTool } from './list.ts' import { applyReadTool, READ_LIMIT, STREAM_MIN_SIZE } from './read.ts' import { applyWriteTool } from './write.ts' import { applyEditTool } from './edit.ts' -import { LIST_MAX_ENTRIES } from './list-render.ts' import { READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from './read-render.ts' import { FsSandboxSurface } from './sandbox.ts' @@ -25,8 +22,6 @@ export const inject = ['tools', 'fs', 'systemPrompt'] /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Maximum entries one `list` page returns; the footer still reports the complete count. */ - listMaxEntries?: number /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ @@ -38,7 +33,6 @@ export interface Config { } export const Config: z = z.object({ - listMaxEntries: z.number().default(LIST_MAX_ENTRIES), readLimit: z.number().default(READ_LIMIT), readMaxLineLength: z.number().default(READ_MAX_LINE_LENGTH), readMaxBytes: z.number().default(READ_MAX_BYTES), @@ -48,23 +42,21 @@ export const Config: z = z.object({ /** The shape after schemastery applied the defaults. */ type ResolvedConfig = Required -/** Every read or listing cap counts lines/chars/bytes/entries — a positive integer, or windowing arithmetic misbehaves silently. */ +/** Every read cap counts lines/chars/bytes — a positive integer, or windowing arithmetic misbehaves silently. */ function assertPositiveInteger(name: string, value: number): void { if (!Number.isInteger(value) || value < 1) { throw new Error(`tool-fs: ${name} must be a positive integer`) } } -/** Register the full `list`/`read`/`write`/`edit` filesystem tool suite. */ +/** Register the full `read`/`write`/`edit` filesystem tool suite. */ export function apply(ctx: Context, config: Config): void { // schemastery (Config) has already filled every defaulted field. const resolved = config as ResolvedConfig - assertPositiveInteger('listMaxEntries', resolved.listMaxEntries) assertPositiveInteger('readLimit', resolved.readLimit) assertPositiveInteger('readMaxLineLength', resolved.readMaxLineLength) assertPositiveInteger('readMaxBytes', resolved.readMaxBytes) assertPositiveInteger('readStreamMinSize', resolved.readStreamMinSize) - applyListTool(ctx, { maxEntries: resolved.listMaxEntries }) applyReadTool(ctx, { limit: resolved.readLimit, maxLineLength: resolved.readMaxLineLength, diff --git a/packages/fs/tool-fs/src/list-render.ts b/packages/fs/tool-fs/src/list-render.ts deleted file mode 100644 index 86b57d02b3..0000000000 --- a/packages/fs/tool-fs/src/list-render.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Pure directory-listing presentation: order direct children, count complete - * composition, and render a bounded page without allowing filesystem text to - * forge the result envelope. - * @module @deepseek-ai/dsh-tool-fs/list-render - */ - -/** Default and maximum number of entries one `list` call returns (the `listMaxEntries` config). */ -export const LIST_MAX_ENTRIES = 200 - -/** One direct child in a directory listing. */ -export interface ListedEntry { - /** Basename of the child inside the listed directory. */ - name: string - /** Whether the child is a regular file, a directory, or something else. */ - type: 'file' | 'directory' | 'other' -} - -/** Complete-listing composition retained on every page. */ -export interface ListCounts { - directories: number - files: number - other: number -} - -/** Canonical bounded result returned by one `list` call. */ -export interface ListPage { - /** Backend display path of the listed directory. */ - path: string - /** 1-based index of the first returned entry. */ - offset: number - /** Current page in directory-first, name-sorted order. */ - entries: ListedEntry[] - /** Number of direct children in the complete listing. */ - totalEntries: number - /** Composition of the complete listing, not only this page. */ - counts: ListCounts -} - -/** - * Sort directories before files before other entries, each group by name. - * @param entries - direct children in provider order. - * @returns a new directory-first array without mutating `entries`. - */ -export function orderEntries(entries: readonly T[]): T[] { - const rank = { directory: 0, file: 1, other: 2 } - return [...entries].sort((a, b) => rank[a.type] - rank[b.type] || a.name.localeCompare(b.name)) -} - -/** - * Count every entry type in a complete listing. - * @param entries - every direct child in the listed directory. - * @returns the complete directory/file/other composition. - */ -export function countEntries(entries: readonly ListedEntry[]): ListCounts { - const counts: ListCounts = { directories: 0, files: 0, other: 0 } - for (const entry of entries) { - if (entry.type === 'directory') counts.directories += 1 - else if (entry.type === 'file') counts.files += 1 - else counts.other += 1 - } - return counts -} - -/** `1 directory` / `4 directories`. */ -function count(n: number, singular: string, plural: string): string { - return `${n} ${n === 1 ? singular : plural}` -} - -/** Complete-listing composition as model-facing prose. */ -function breakdown(counts: ListCounts): string { - const parts = [ - count(counts.directories, 'directory', 'directories'), - count(counts.files, 'file', 'files'), - ] - if (counts.other > 0) parts.push(`${counts.other} other`) - return parts.join(', ') -} - -/** - * Names this renderer cannot emit verbatim, because POSIX allows every byte but - * `/` and NUL in a name and each of these would make the listing say something - * untrue: - * - * - a control character (a newline above all) splits one entry across lines; - * - ` 1 || page.entries.length < page.totalEntries - ? `(Showing entries ${page.offset}-${end} of ${page.totalEntries}: ${breakdown(page.counts)}.` - + (end < page.totalEntries ? ` Use offset=${end + 1} to continue.)` : ')') - : `(${count(page.totalEntries, 'entry', 'entries')}: ${breakdown(page.counts)})` - const body = page.entries.length > 0 - ? `${page.entries.map(entry => `${renderName(entry.name)}${suffix[entry.type]}`).join('\n')}\n\n${footer}` - : footer - return `${renderName(page.path)} -directory - -${body} -` -} diff --git a/packages/fs/tool-fs/src/list.ts b/packages/fs/tool-fs/src/list.ts deleted file mode 100644 index 270e2803b8..0000000000 --- a/packages/fs/tool-fs/src/list.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Model-facing directory listing. It enumerates ONE directory level through the - * provider seam's `listDir`, orders children so a capped view keeps the - * navigable structure, and renders the entries with their type. - * - * This is the orientation tool: `glob` and `grep` answer "where is the thing I - * can already name", while `list` answers "what is here at all". `rg --files` - * never emits directories, so no pattern makes `glob` describe a directory's - * shape — the gap this tool closes. - * @module @deepseek-ai/dsh-tool-fs/list - */ - -import type { Context } from 'cordis' -import { defineTool } from '@deepseek-ai/dsh-tools' -import type { GenericCallView } from '@deepseek-ai/dsh-tools' -import { FsError } from '@deepseek-ai/dsh-fs' -import type {} from '@deepseek-ai/dsh-fs' -import type {} from '@deepseek-ai/dsh-system-prompt' -import { countEntries, formatListOutput, orderEntries } from './list-render.ts' -import { sessionResolveOptions } from './session-cwd.ts' - -/** Resolved list-tool caps — plugin config after defaulting (see `Config` in index.ts). */ -export interface ListToolCaps { - /** Maximum entries returned on one page; the footer still reports complete size and composition. */ - maxEntries: number -} - -/** Validated `list` arguments after defaulting. */ -export interface ListInput { - /** Directory to list; `.` means the calling agent's session workspace. */ - path: string - /** 1-based first entry to return from the directory-first ordering. */ - offset: number -} - -/** - * Validate value constraints the schema DSL can't express, and default an - * omitted `path` to `.` — the session workspace, so "what is in this project" - * needs no argument at all. - * - * @param args - the schema-validated `list` arguments. - * @returns the accepted input with `path` and `offset` defaulted. - */ -export function parseListArgs(args: { path?: string; offset?: number }): ListInput { - if (args.path !== undefined && args.path.trim().length === 0) throw new Error('path must be a non-empty string when given') - const offset = args.offset ?? 1 - if (!Number.isInteger(offset) || offset < 1) throw new Error('offset must be a positive integer') - return { path: args.path ?? '.', offset } -} - -/** - * Pending-call presentation: a generic card titled by the directory, with a - * follow-along location so a capable editor can reveal it. - * - * @param args - the raw tool arguments; `path` and `offset` feed the title. - * @returns the generic card view shown while the call runs. - */ -export function presentListCall(args: { path?: string; offset?: number }): GenericCallView { - const path = args.path ?? '.' - const window = args.offset !== undefined ? ` (from entry ${args.offset})` : '' - return { card: 'generic', title: `List ${path}${window}`, kind: 'read', locations: [{ path }] } -} - -/** - * Register the `list` tool and its system-prompt guidance. - * - * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service. - * @param caps - the deployment's resolved list caps (plugin config after defaulting). - */ -export function applyListTool(ctx: Context, caps: ListToolCaps): void { - ctx.systemPrompt.section({ - name: 'tool:list', - order: 99, - text: 'Use the list tool — not shell ls — to see what a directory contains. It returns the direct children of one directory, files and subdirectories alike, ' - + 'and defaults to the session workspace, so it is the first step for orienting in an unfamiliar project. ' - + 'When a result is capped, continue with the offset named in its footer.', - }) - - ctx.tools.register(defineTool({ - name: 'list', - description: 'List the direct children of one directory, with their type. ' - + `Entries are directories first, then files, each alphabetical; up to ${caps.maxEntries} are returned from the requested offset, and the footer gives the complete count plus a next offset when more remain. ` - + 'It includes subdirectories and is the tool for seeing one directory\'s contents.', - parameters: { - path: { type: 'string', description: 'Directory to list. Defaults to the session workspace; a relative path resolves against it.' }, - offset: { type: 'number', description: '1-based first entry to return. Defaults to 1; use the footer value to continue.' }, - }, - output: { - schema: { - type: 'object', - additionalProperties: false, - properties: { - path: { type: 'string', required: true }, - offset: { type: 'integer', required: true }, - entries: { - type: 'array', - required: true, - items: { - type: 'object', - additionalProperties: false, - properties: { - name: { type: 'string', required: true }, - type: { type: 'string', required: true, enum: ['file', 'directory', 'other'] }, - }, - }, - }, - totalEntries: { type: 'integer', required: true }, - counts: { - type: 'object', - required: true, - additionalProperties: false, - properties: { - directories: { type: 'integer', required: true }, - files: { type: 'integer', required: true }, - other: { type: 'integer', required: true }, - }, - }, - }, - }, - render: (_args, value) => [{ type: 'text', text: formatListOutput(value) }], - }, - // Listing reads directory metadata only: no content, no version recorded, - // nothing a concurrent call could observe out of order. - isConcurrencySafe: () => true, - async execute(args, exec) { - const input = parseListArgs(args) - const target = await ctx.fs.resolve(input.path, sessionResolveOptions(exec, input.path)) - // No stat first: the seam already answers absence with FS_NOT_FOUND and a - // non-directory target with FS_NOT_DIRECTORY, so a probe would only add a - // round-trip and a second source of truth. (0 stat.) - const entries = orderEntries(await ctx.fs.listDir(target, exec.signal)) - if (input.offset > entries.length && !(entries.length === 0 && input.offset === 1)) { - throw new FsError( - `offset ${input.offset} is out of range for "${target.displayPath}" (${entries.length} entries)`, - 'FS_NOT_FOUND', - ) - } - return { - path: target.displayPath, - offset: input.offset, - entries: entries - .slice(input.offset - 1, input.offset - 1 + caps.maxEntries) - .map(({ name, type }) => ({ name, type })), - totalEntries: entries.length, - counts: countEntries(entries), - } - }, - presentCall: presentListCall, - })) -} diff --git a/packages/fs/tool-fs/tests/list-render.spec.ts b/packages/fs/tool-fs/tests/list-render.spec.ts deleted file mode 100644 index 792c91bd18..0000000000 --- a/packages/fs/tool-fs/tests/list-render.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Pure listing-presentation tests: display ordering and the model-facing - * envelope, exercised without a context or provider. - */ - -import { describe, expect, it } from 'vitest' -import { countEntries, formatListOutput, orderEntries } from '../src/list-render.ts' -import type { ListedEntry, ListPage } from '../src/list-render.ts' - -const entry = (name: string, type: ListedEntry['type'] = 'file'): ListedEntry => ({ name, type }) - -function page(entries: ListedEntry[], options: { offset?: number; totalEntries?: number; all?: ListedEntry[] } = {}): ListPage { - const all = options.all ?? entries - return { - path: '/w', - offset: options.offset ?? 1, - entries, - totalEntries: options.totalEntries ?? all.length, - counts: countEntries(all), - } -} - -describe('orderEntries', () => { - it('groups directories, then files, then other, each by name', () => { - const ordered = orderEntries([ - entry('zeta.txt'), - entry('socket', 'other'), - entry('beta'), - entry('src', 'directory'), - entry('assets', 'directory'), - ]) - expect(ordered.map(e => e.name)).toEqual(['assets', 'src', 'beta', 'zeta.txt', 'socket']) - }) - - it('leaves the input array untouched and preserves extra entry fields', () => { - const input = [{ name: 'b', type: 'file' as const, size: 2 }, { name: 'a', type: 'file' as const, size: 1 }] - const ordered = orderEntries(input) - expect(input.map(e => e.name)).toEqual(['b', 'a']) - expect(ordered).toEqual([{ name: 'a', type: 'file', size: 1 }, { name: 'b', type: 'file', size: 2 }]) - }) -}) - -describe('formatListOutput', () => { - it('marks directories and non-regular children, and counts the whole listing', () => { - expect(formatListOutput(page([entry('src', 'directory'), entry('a.txt'), entry('sock', 'other')]))).toBe(`/w -directory - -src/ -a.txt -sock@ - -(3 entries: 1 directory, 1 file, 1 other) -`) - }) - - it('omits the "other" clause when every child is a file or a directory', () => { - expect(formatListOutput(page([entry('a.txt'), entry('b.txt')]))).toContain('(2 entries: 0 directories, 2 files)') - }) - - it('says a one-entry listing in the singular', () => { - expect(formatListOutput(page([entry('only', 'directory')]))).toContain('(1 entry: 1 directory, 0 files)') - }) - - it('states the complete size and composition when the view is capped', () => { - const entries = [entry('src', 'directory'), ...Array.from({ length: 5 }, (_, i) => entry(`f${i}.txt`))] - const rendered = formatListOutput(page(entries.slice(0, 2), { totalEntries: entries.length, all: entries })) - expect(rendered).toContain('src/\nf0.txt\n') - expect(rendered).not.toContain('f2.txt') - expect(rendered).toContain('(Showing entries 1-2 of 6: 1 directory, 5 files. Use offset=3 to continue.)') - }) - - it('renders an empty directory as a footer alone', () => { - expect(formatListOutput(page([]))).toBe(`/w -directory - -(Empty directory) -`) - }) -}) diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index 53b58d2dcf..de844dcaf4 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -38,7 +38,6 @@ const testToolSignal = new AbortController().signal class FakeFs extends FileSystem { files = new Map() rejectWith?: FsError - dirs = new Map() writeIntents: (FsWriteIntent | undefined)[] = [] editIntents: ({ version: FsVersion } | undefined)[] = [] @@ -67,9 +66,8 @@ class FakeFs extends FileSystem { const content = this.files.get(target.targetKey) ?? '' return (async function* () { yield content })() } - override async listDir(target: FsTarget): Promise { - this.throwIfArmed() - return this.dirs.get(target.targetKey) ?? [] + override async listDir(_target: FsTarget): Promise { + return [] } override async writeText(target: FsTarget, content: string, expected?: FsWriteIntent): Promise { this.throwIfArmed() @@ -141,15 +139,13 @@ describe('session cwd resolution', () => { }) describe('registration', () => { - it('registers list, read, write, and edit', async () => { + it('registers read, write, and edit', async () => { const { ctx } = await setup() - expect(ctx.tools.schemas().map(s => s.name).sort()).toEqual(['edit', 'list', 'read', 'write']) + expect(ctx.tools.schemas().map(s => s.name).sort()).toEqual(['edit', 'read', 'write']) }) - it('declares list and read parallel-safe while write/edit remain exclusive', async () => { + it('declares read parallel-safe while write/edit remain exclusive', async () => { const { ctx } = await setup() - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('list-safe'), name: 'list', arguments: {} })) - .toEqual({ kind: 'parallel' }) expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } })) .toEqual({ kind: 'parallel' }) expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } })) @@ -161,8 +157,6 @@ describe('registration', () => { it('registers prompt sections for each tool', async () => { const { ctx } = await setup() const prompt = renderPrompt(await ctx.systemPrompt.assemble()) - expect(prompt).toContain('Use the list tool') - expect(prompt).not.toContain('glob or grep') expect(prompt).toContain('Use the read tool') expect(prompt).toContain('Use the write tool') expect(prompt).toContain('Use the edit tool') @@ -185,10 +179,9 @@ describe('registration', () => { const fiber = await ctx.plugin(ToolFs) // Each tool contributes BOTH a schema and a prompt section; disposal must // withdraw both, not just the schemas. - expect(ctx.tools.schemas()).toHaveLength(4) + expect(ctx.tools.schemas()).toHaveLength(3) const sectionNames = (a: { sections: { name: string }[] }) => a.sections.map(s => s.name).sort() - expect(sectionNames(await ctx.systemPrompt.assemble())) - .toEqual(['deployment:persona', 'harness:identity', 'tool:edit', 'tool:list', 'tool:read', 'tool:write']) + expect(sectionNames(await ctx.systemPrompt.assemble())).toEqual(['deployment:persona', 'harness:identity', 'tool:edit', 'tool:read', 'tool:write']) await fiber.dispose() expect(ctx.tools.schemas()).toHaveLength(0) // Only the system-prompt plugin's own built-in sections remain. @@ -196,144 +189,6 @@ describe('registration', () => { }) }) -describe('list tool', () => { - /** Seed one directory's children; `listDir` order is deliberately NOT display order. */ - function seedDir(fs: FakeFs, path: string, children: readonly { name: string; type: 'file' | 'directory' | 'other' }[]): void { - fs.dirs.set(`key:${path}`, children.map(({ name, type }) => ({ - name, - type, - target: { targetKey: FsTargetKey(`key:${path}/${name}`), displayPath: `/abs/${path}/${name}` }, - }))) - } - - it('defaults to the session workspace and shows directories before files', async () => { - const { ctx, fs } = await setup() - seedDir(fs, '.', [ - { name: 'notes.md', type: 'file' }, - { name: 'zeroomega-3.3.23', type: 'directory' }, - { name: 'archive', type: 'directory' }, - { name: 'link-to-nowhere', type: 'other' }, - ]) - const result = await call(ctx, 'list', {}) - expect(result.isError).toBe(false) - if (result.isError) throw new Error('expected list success') - // The canonical value carries display order, so a Code Mode caller and the - // model see the same ordering contract. - expect(result.value).toEqual({ - path: '/abs/.', - offset: 1, - entries: [ - { name: 'archive', type: 'directory' }, - { name: 'zeroomega-3.3.23', type: 'directory' }, - { name: 'notes.md', type: 'file' }, - { name: 'link-to-nowhere', type: 'other' }, - ], - totalEntries: 4, - counts: { directories: 2, files: 1, other: 1 }, - }) - expect(text(result)).toBe(`/abs/. -directory - -archive/ -zeroomega-3.3.23/ -notes.md -link-to-nowhere@ - -(4 entries: 2 directories, 1 file, 1 other) -`) - }) - - it('lists an explicit path and reports an empty directory as such', async () => { - const { ctx, fs } = await setup() - seedDir(fs, 'empty', []) - const result = await call(ctx, 'list', { path: 'empty' }) - expect(text(result)).toContain('(Empty directory)') - expect(text(result)).toContain('/abs/empty') - }) - - it('caps the rendered entries but still reports the complete composition', async () => { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(FakeFs) - await ctx.plugin(ToolFs, { listMaxEntries: 2 }) - const fs = ctx.fs as FakeFs - seedDir(fs, '.', [ - { name: 'a.txt', type: 'file' }, - { name: 'b.txt', type: 'file' }, - { name: 'c.txt', type: 'file' }, - { name: 'src', type: 'directory' }, - ]) - const result = await call(ctx, 'list', {}) - const rendered = text(result) - // The one directory survives the cap because directories sort first — the - // failure mode this ordering exists to prevent. - expect(rendered).toContain('src/\na.txt\n') - expect(rendered).not.toContain('c.txt') - expect(rendered).toContain('(Showing entries 1-2 of 4: 1 directory, 3 files. Use offset=3 to continue.)') - if (result.isError) throw new Error('expected list success') - expect(result.value).toEqual({ - path: '/abs/.', - offset: 1, - entries: [{ name: 'src', type: 'directory' }, { name: 'a.txt', type: 'file' }], - totalEntries: 4, - counts: { directories: 1, files: 3, other: 0 }, - }) - - const continuation = await call(ctx, 'list', { offset: 3 }) - expect(text(continuation)).toContain('b.txt\nc.txt') - expect(text(continuation)).toContain('(Showing entries 3-4 of 4: 1 directory, 3 files.)') - }) - - it('rejects a blank path and surfaces provider failures', async () => { - const { ctx, fs } = await setup() - const blank = await call(ctx, 'list', { path: ' ' }) - expect(blank.isError).toBe(true) - expect(text(blank)).toContain('path must be a non-empty string when given') - - fs.rejectWith = new FsError('cannot list "/abs/a.txt": not a directory', 'FS_NOT_DIRECTORY') - const failed = await call(ctx, 'list', { path: 'a.txt' }) - expect(failed.isError).toBe(true) - expect(failed.error).toMatchObject({ info: { code: 'FS_NOT_DIRECTORY' } }) - }) - - it('rejects invalid and out-of-range continuation offsets', async () => { - const { ctx, fs } = await setup() - seedDir(fs, '.', [{ name: 'only.txt', type: 'file' }]) - expect(text(await call(ctx, 'list', { offset: 0 }))).toContain('offset must be a positive integer') - expect(text(await call(ctx, 'list', { offset: 1.5 }))).toContain('offset must be a positive integer') - expect(text(await call(ctx, 'list', { offset: 2 }))).toContain('offset 2 is out of range') - }) - - it('encodes filesystem names without allowing them to forge the envelope or type marker', async () => { - const { ctx, fs } = await setup() - seedDir(fs, '.', [ - { name: 'regular@', type: 'file' }, - { name: 'special', type: 'other' }, - { name: 'fake\n', type: 'file' }, - ]) - const rendered = text(await call(ctx, 'list', {})) - // A regular file really named `regular@` must not read as a socket named - // `regular`, and a newline in a name must not become a second entry. - expect(rendered).toContain('"regular@"\nspecial@') - expect(rendered).toContain('"fake\\n<\\/content>"') - expect(rendered.match(/<\/content>/g)).toHaveLength(1) - }) - - it('records no observation, so a listing never authorizes a mutation', async () => { - const { ctx, fs } = await setup() - fs.files.set('key:a.txt', 'hello') - seedDir(fs, '.', [{ name: 'a.txt', type: 'file' }]) - const observed = vi.fn() - ctx.on('fs/observed', observed) - await call(ctx, 'list', {}) - expect(observed).not.toHaveBeenCalled() - // Seeing a name is not reading a file: the policy gate still demands a read. - const edit = await call(ctx, 'edit', { file_path: 'a.txt', old_string: 'h', new_string: 'j' }, { session: { header: {} } }) - expect(edit.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } }) - }) -}) - describe('read tool', () => { it('formats line-numbered content with a footer', async () => { const { ctx, fs } = await setup() @@ -589,18 +444,6 @@ describe('tool-owned presentation (pure presentCall)', () => { }) }) - it('list: titles by the directory, falling back to the workspace "." when unset', async () => { - expect(await presentCall('list', { path: 'src' })).toEqual({ - card: 'generic', title: 'List src', kind: 'read', locations: [{ path: 'src' }], - }) - expect(await presentCall('list', {})).toEqual({ - card: 'generic', title: 'List .', kind: 'read', locations: [{ path: '.' }], - }) - expect(await presentCall('list', { path: 'src', offset: 201 })).toEqual({ - card: 'generic', title: 'List src (from entry 201)', kind: 'read', locations: [{ path: 'src' }], - }) - }) - it('read: bare title and line-1 location when offset/limit are unset', async () => { expect(await presentCall('read', { file_path: 'a.txt' })).toEqual({ card: 'generic', title: 'Read a.txt', kind: 'read', locations: [{ path: 'a.txt', line: 1 }], @@ -813,7 +656,6 @@ describe('read caps are plugin config', () => { }) it.each([ - ['listMaxEntries', { listMaxEntries: 0 }], ['readLimit', { readLimit: 0 }], ['readLimit', { readLimit: 2.5 }], ['readMaxLineLength', { readMaxLineLength: -1 }], diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index c507ae0e0d..7bdc8b68a8 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -230,7 +230,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(ToolFs) }, note: - 'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. `list` paginates one directory level through `ctx.fs.listDir` and records no observation, so seeing a filename never satisfies that read-before-write gate.', + 'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.', }, { pkg: '@deepseek-ai/dsh-tool-fs-search', @@ -248,7 +248,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(ToolFsSearch) }, note: - 'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. An over-cap glob result does not return the head of the sorted list: its inline page is taken round-robin across entries immediately beneath the actual search root and the footer states that basis, because modification-time order is ascending and the restored timestamps of an unpacked archive put one subtree in front of every broad match.', + 'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.', }, { pkg: '@deepseek-ai/dsh-tool-pty',