mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/feat/ripgrep-packaged-binary' into feat/pwsh-tool
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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/architecture/2026-08-01-packaged-ripgrep-search.md
|
||||
2026-08-01-packaged-ripgrep-search.md: 0470623163ff7686bdd77ca715bac68c9a466d2b
|
||||
2026-08-01-packaged-ripgrep-search.zh.md: fe53643aff139615d49edd2e3b27d14428cd2029
|
||||
2026-08-01-packaged-ripgrep-search.md: 849cc0804a2081492297649ac8f13236e2ac60fe
|
||||
2026-08-01-packaged-ripgrep-search.zh.md: 4381c3a8bca8e6bb9ab375ad352fec5a5ddb9f99
|
||||
|
||||
@@ -12,7 +12,7 @@ The `glob`/`grep` tools ran through the bash executor seam, which made a system
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-tool-fs-search` now runs the PACKAGED ripgrep binary (`@vscode/ripgrep`, an npm dependency whose optional platform packages ship the binary) through the `ctx.subprocess` seam: `runRipgrep()` spawns `rgPath` with a plain argv vector, collect-mode stdout/stderr, `graceMs`, and `exec.signal` forwarded. There is no shell layer, so the shell-quoting boundary is gone from execution; `singleQuote` stays exported as a compatibility surface with its tests. Registration is unconditional — the load-time `command -v rg` probe and the conditional registration decision are deleted, and with them the "rg not found" warning. The package injects `tools`, `systemPrompt`, and `subprocess`.
|
||||
`@deepseek-ai/dsh-tool-fs-search` now runs the PACKAGED ripgrep binary (`@vscode/ripgrep`, an npm dependency whose optional platform packages ship the binary) through the `ctx.subprocess` seam: `runRipgrep()` spawns `rgPath` with a plain argv vector prefixed by `--no-config`, collect-mode stdout/stderr, `graceMs`, and `exec.signal` forwarded. There is no shell layer, so the shell-quoting boundary is gone from execution; the `singleQuote` helper and its shell-spawning tests are deleted with it. The raw streams request the seam's diagnostic-tail collect shape (no spill files — the tool never reads a raw spill path; a lossy stdout read fails as `SEARCH_RAW_OUTPUT_OVERFLOW`). The terminate grace and the stderr tail budget are validated `Config` fields (`graceMs` default 3000, `stderrMaxBytes` default 64 KiB), no longer inherited from bash-local's config. Registration is unconditional — the load-time `command -v rg` probe and the conditional registration decision are deleted, and with them the "rg not found" warning. The package injects `tools`, `systemPrompt`, and `subprocess`.
|
||||
|
||||
Exit semantics stay tool-owned: exit 0 is success with results, exit 1 is a successful empty search, anything else classifies into the existing `SEARCH_*` vocabulary (invalid pattern, launch failure, signal kill, raw-output overflow). Timeout is the cooperative tool-call budget attached to the tool definitions: `@deepseek-ai/dsh-timeout-policy` aborts `exec.signal`, the subprocess seam's terminate escalation provides the hard kill, and the tool reports `SEARCH_ABORTED`. The working directory is the session header cwd when present, else `process.cwd()` — there is no executor config to default through anymore, so the tool owns the fallback.
|
||||
|
||||
@@ -30,6 +30,8 @@ The `fs-glob-sampling` ACP snapshot scenario now executes the real packaged bina
|
||||
|
||||
- The discovery tools work on every platform the packaged binary covers (darwin/linux/win32, x64/arm64) with no host install; the shipped TUI/Web rosters gain `glob`/`grep` as fixed members ([even-out-shipped-tool-rosters](../feature/2026-07-31-even-out-shipped-tool-rosters.md)).
|
||||
- The shell-string attack surface is gone: hostile patterns are inert argv elements, pinned by the integration suite, which now runs on Windows too (it previously self-skipped without a system `rg`).
|
||||
- The spawn is unconfined (a plain `ctx.subprocess` call), so `--no-config` is prepended: a host `RIPGREP_CONFIG_PATH` (or an `rg.conf` beside the binary) can otherwise inject a `--pre` preprocessor that executes an arbitrary command for every matched file. With `--no-config`, no config file — and therefore no preprocessor — can reach the search.
|
||||
- The raw-output overflow path changed shape: the old bash-backed route inherited bash-local's always-on spill and could leave an unread multi-megabyte temp file; the subprocess seam now collects without spill, and overflow is a pure error (`SEARCH_RAW_OUTPUT_OVERFLOW`, "narrow pattern, path, or include and retry") with zero content returned.
|
||||
- Load-time failure modes changed: a broken subprocess seam now fails the first search call (`SEARCH_FAILED`) instead of failing plugin load through the probe; a missing binary is a launch failure with the packaged path, not a PATH problem.
|
||||
- The integration suite's fixture dropped a filename Windows cannot represent (`"` in a name), keeping the suite replayable on every platform.
|
||||
- Regenerating `THIRD_PARTY_NOTICES.md` surfaced a latent generator bug the new dependency made visible: Node's `fs.globSync` returns OS-native separators, so on Windows the `/`-suffixed dev-area prefixes in the notices tiering never matched and dev-only packages (test tooling, support leaves) were mis-tiered as runtime. The generator now normalizes manifest paths at ingestion, and the notices are platform-independent.
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-tool-fs-search` 现在运行 PACKAGED(打包的)ripgrep 二进制(`@vscode/ripgrep`,一个 npm 依赖,其可选平台包随附二进制),经由 `ctx.subprocess` seam:`runRipgrep()` 以纯 argv 向量 spawn `rgPath`,配以 collect 模式 stdout/stderr、`graceMs` 与转发的 `exec.signal`。不再有 shell 层,执行路径上的 shell 引号边界随之消失;`singleQuote` 作为兼容导出与其测试保留。注册变为无条件——加载期 `command -v rg` 探针与条件注册决策被删除,连同那条 "rg not found" 警告。本包注入 `tools`、`systemPrompt` 与 `subprocess`。
|
||||
`@deepseek-ai/dsh-tool-fs-search` 现在运行 PACKAGED(打包的)ripgrep 二进制(`@vscode/ripgrep`,一个 npm 依赖,其可选平台包随附二进制),经由 `ctx.subprocess` seam:`runRipgrep()` 以纯 argv 向量 spawn `rgPath`,向量前缀 `--no-config`,配以 collect 模式 stdout/stderr、`graceMs` 与转发的 `exec.signal`。不再有 shell 层,执行路径上的 shell 引号边界随之消失;`singleQuote` 工具与其 shell spawn 测试一并删除。原始流使用 seam 的诊断尾部 collect 形态(无 spill 文件——工具从不读取原始 spill 路径;lossy stdout 读取以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败)。终止宽限与 stderr 尾部预算成为经校验的 `Config` 字段(`graceMs` 默认 3000,`stderrMaxBytes` 默认 64 KiB),不再继承自 bash-local 的配置。注册变为无条件——加载期 `command -v rg` 探针与条件注册决策被删除,连同那条 "rg not found" 警告。本包注入 `tools`、`systemPrompt` 与 `subprocess`。
|
||||
|
||||
退出语义仍由工具拥有:退出码 0 为有结果的成功,1 为成功的空搜索,其余归入既有 `SEARCH_*` 词汇(无效模式、启动失败、信号杀死、原始输出溢出)。超时是挂在工具定义上的协作式工具调用预算:`@deepseek-ai/dsh-timeout-policy` 中止 `exec.signal`,subprocess seam 的终止升级提供硬终止,工具报告 `SEARCH_ABORTED`。工作目录为会话 header cwd(存在时),否则为 `process.cwd()`——不再有执行器配置可供默认化,因此回退由工具自己拥有。
|
||||
|
||||
@@ -30,6 +30,8 @@ Status: implemented
|
||||
|
||||
- 发现工具在打包二进制覆盖的每个平台(darwin/linux/win32,x64/arm64)上开箱即用,无需宿主安装;交付的 TUI/Web 工具清单把 `glob`/`grep` 变为固定成员(见 [拉平交付的工具清单](../feature/2026-07-31-even-out-shipped-tool-rosters.md))。
|
||||
- shell 字符串攻击面消失:恶意模式只是惰性 argv 元素,由集成套件钉住;该套件现在也在 Windows 上运行(此前没有系统 `rg` 时它自行跳过)。
|
||||
- spawn 不受沙箱约束(普通的 `ctx.subprocess` 调用),因此前缀 `--no-config`:宿主的 `RIPGREP_CONFIG_PATH`(或二进制旁的 `rg.conf`)否则可注入 `--pre` 预处理器,对每个匹配文件执行任意命令。加上 `--no-config` 后,任何配置文件——因而任何预处理器——都无法触及搜索。
|
||||
- 原始输出溢出路径的形态改变:旧的 bash 承载路径继承了 bash-local 常开的 spill,可能留下没人读的多 MB 临时文件;subprocess seam 现在无 spill 收集,溢出是纯粹的错误(`SEARCH_RAW_OUTPUT_OVERFLOW`,"narrow pattern, path, or include and retry"),不返回任何内容。
|
||||
- 加载期失败模式改变:subprocess seam 损坏现在让首次搜索调用失败(`SEARCH_FAILED`),而非通过探针使插件加载失败;二进制缺失是带打包路径的启动失败,而不是 PATH 问题。
|
||||
- 集成套件的 fixture 去掉了 Windows 无法表示的文件名(名称含 `"`),保证套件在每个平台都能重放。
|
||||
- 重新生成 `THIRD_PARTY_NOTICES.md` 暴露了一个由新依赖带出的潜在生成器 bug:Node 的 `fs.globSync` 返回操作系统原生分隔符,因此在 Windows 上 notices 分层中带 `/` 后缀的 dev 区前缀永远匹配不上,dev-only 包(测试工具、support 叶子)被错分为 runtime。生成器现在在入口处归一化清单路径,notices 与平台无关。
|
||||
|
||||
@@ -1763,12 +1763,16 @@ export interface Config {
|
||||
searchMetaMaxBytes?: number
|
||||
/** Max complete raw `rg` stdout bytes a search will parse; larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. */
|
||||
rawOutputMaxBytes?: number
|
||||
/** Terminate-escalation grace period (ms) for one search process, handed to the subprocess seam. */
|
||||
graceMs?: number
|
||||
/** Max bytes retained for one search's stderr diagnostic tail (never surfaced to the model). */
|
||||
stderrMaxBytes?: number
|
||||
/** Cooperative tool-call timeout budget (ms) on both tools, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`. */
|
||||
timeoutMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/fs/tool-fs-search/src/index.ts:70`](../packages/fs/tool-fs-search/src/index.ts)
|
||||
Source: [`packages/fs/tool-fs-search/src/index.ts:71`](../packages/fs/tool-fs-search/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-tool-goal`
|
||||
|
||||
|
||||
@@ -181,16 +181,23 @@ const SCENARIOS: Scenario[] = [
|
||||
// `--sort=modified` order, pinning over-cap glob sampling without depending
|
||||
// on a host-installed ripgrep binary or a PATH stand-in. POSIX-only because
|
||||
// the displayed paths carry `/` separators the session-log comparison
|
||||
// cannot normalize.
|
||||
// cannot normalize. Recorded (not authored): the assistant turn is a real
|
||||
// model transcript; re-record with `test:snapshot:record -t fs-glob-sampling`.
|
||||
// The composition disables packed chunk rows (fs-search.cordis.yml), whose
|
||||
// run boundaries depend on eager-drain timing, and the recorded fixture's
|
||||
// `request/header` config and `request/context` are normalized to the
|
||||
// replay-produced minimal shape (the live adapter logs model capabilities
|
||||
// like maxTokens/reasoningEffort that llm-replay has no data for), and its
|
||||
// tool-result paths are canonicalized to `/` separators.
|
||||
{
|
||||
name: 'fs-glob-sampling',
|
||||
hasModelTurn: true,
|
||||
recorded: false,
|
||||
recorded: true,
|
||||
posixOnly: true,
|
||||
pinsHeader: true,
|
||||
headerClass: 'fs-search',
|
||||
configPath: FS_SEARCH_CONFIG,
|
||||
prepareWorkspace: prepareFsSearchWorkspace,
|
||||
posixOnly: true,
|
||||
},
|
||||
{ name: 'fs-read', hasModelTurn: true, recorded: true },
|
||||
{ name: 'fs-write', hasModelTurn: true, recorded: true },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
providers:
|
||||
- id: deepseek
|
||||
- id: deepseek-official
|
||||
name: DeepSeek
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
@@ -17,10 +17,14 @@
|
||||
- id: acp-agent
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: none
|
||||
# Unpacked rows: the eager-drain batch boundaries that split packed delta
|
||||
# runs are timing-dependent, so packed logs cannot replay-match a live
|
||||
# record of a long reasoning stream.
|
||||
packChunks: false
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
- id: acp-agent
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
# Unpacked rows: the eager-drain batch boundaries that split packed delta
|
||||
# runs are timing-dependent, so packed logs cannot replay-match a live
|
||||
# record of a long reasoning stream.
|
||||
packChunks: false
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
|
||||
@@ -1,25 +1,120 @@
|
||||
{"type":"session","version":0,"id":"f5a99d52-3eaa-4ce7-858d-61d4fd77df2a","createdAt":1785218400000,"cwd":"{{cwd}}","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 path tree. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"},"role":"user","id":"6790985f-1de2-42f8-a7f1-24e46d6439c7"},"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":"request/context","seq":5,"time":1785483397569,"data":{"provider":"deepseek","model":"deepseek-v4-pro"}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1785218400007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1785218400008,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"glob-sampling-call","name":"glob","argumentsDelta":"{\"pattern\":\"*\",\"path\":\"tree\"}"}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1785218400009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\",\"path\":\"tree\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":1785218400010,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1,"outputTokens":1}}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":1785483397579,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":11,"time":1785483397579,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\",\"path\":\"tree\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-pro"},"id":"a127cfe5-39fb-462c-8e5a-a8c79bd0e52b"},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[6,7,8,9,10],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":12,"time":1785483397579,"data":{"turn":1,"step":1,"callId":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\",\"path\":\"tree\"}"}}
|
||||
{"type":"tool/result","seq":13,"time":1785483398062,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"glob-sampling-call"},"content":[{"type":"tool-result","toolCallId":"glob-sampling-call","content":[{"type":"text","text":"tree/archive/a.ts\ntree/docs/guide.md\ntree/src/index.ts\ntree/test/spec.ts\n\n(Showing 4 of 8 paths, sampled across 4 of the 6 top-level entries this pattern matched instead of taken in modification-time order. Narrow path to inspect a specific subtree. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"2beecb2e-627d-43dc-a936-03e1dc874093"},"meta":{"shape":"paths","paths":["tree/archive/a.ts","tree/docs/guide.md","tree/src/index.ts","tree/test/spec.ts"],"truncated":true,"total":8}},"sourceEventSeqs":[12],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":14,"time":1785483398062,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":15,"time":1785483398072,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":16,"time":1785218400017,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":1785218400018,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"GLOB_SAMPLED"}}}
|
||||
{"type":"assistant/chunk","seq":18,"time":1785218400019,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GLOB_SAMPLED"}}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":1785218400020,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":1,"outputTokens":1}}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":1785483398078,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":21,"time":1785483398078,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"GLOB_SAMPLED"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-pro"},"id":"ce2334a4-be71-490b-a502-29186a9ced5c"},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":22,"time":1785483398078,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":23,"time":1785483398079,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session","version":0,"id":"4428b809-66d5-4ea2-9a03-89de742fcda1","createdAt":1785591986068,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1785591986072,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785591986073,"data":{"content":[{"type":"text","text":"Call glob exactly once with pattern * and path tree. Then reply with exactly GLOB_SAMPLED and nothing else."}],"source":{"kind":"user"},"role":"user","id":"3d05fb76-4185-460b-9c6a-8c1b2495bc9f"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785591986074,"data":{"title":"Call glob exactly once with","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785591986092,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785591986093,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":5,"time":1785591986094,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1785591987500,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1785591987500,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1785591987529,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":1785591987587,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":1785591987588,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":1785591987588,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}
|
||||
{"type":"assistant/chunk","seq":12,"time":1785591987588,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" call"}}}
|
||||
{"type":"assistant/chunk","seq":13,"time":1785591987588,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" glob"}}}
|
||||
{"type":"assistant/chunk","seq":14,"time":1785591987639,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":1785591987639,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":1785591987639,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":1785591987685,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" pattern"}}}
|
||||
{"type":"assistant/chunk","seq":18,"time":1785591987685,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" *"}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":1785591987876,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" path"}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tree"}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}}
|
||||
{"type":"assistant/chunk","seq":24,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1785591987877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"G"}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"LOB"}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_S"}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"AM"}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"PL"}}}
|
||||
{"type":"assistant/chunk","seq":33,"time":1785591987878,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ED"}}}
|
||||
{"type":"assistant/chunk","seq":34,"time":1785591987977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}}
|
||||
{"type":"assistant/chunk","seq":35,"time":1785591988034,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":36,"time":1785591988035,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":""}}}
|
||||
{"type":"assistant/chunk","seq":37,"time":1785591988090,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"{"}}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1785591988090,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1785591988090,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"pattern"}}}
|
||||
{"type":"assistant/chunk","seq":40,"time":1785591988091,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":41,"time":1785591988136,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":": "}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":1785591988136,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":43,"time":1785591988136,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"*"}}}
|
||||
{"type":"assistant/chunk","seq":44,"time":1785591988193,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":45,"time":1785591988207,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":", "}}}
|
||||
{"type":"assistant/chunk","seq":46,"time":1785591988207,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":47,"time":1785591988207,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"path"}}}
|
||||
{"type":"assistant/chunk","seq":48,"time":1785591988207,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":49,"time":1785591988207,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":": "}}}
|
||||
{"type":"assistant/chunk","seq":50,"time":1785591988284,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":51,"time":1785591988284,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"tree"}}}
|
||||
{"type":"assistant/chunk","seq":52,"time":1785591988284,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":53,"time":1785591988338,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","argumentsDelta":"}"}}}
|
||||
{"type":"assistant/chunk","seq":54,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."}}}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":1785591988427,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":58,"time":1785591988430,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to call glob exactly once with pattern * and path tree, then reply with exactly \"GLOB_SAMPLED\"."},{"type":"tool-call","id":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"b74cbab2-c017-4e44-8c09-a7745d8b274a"},"usage":{"inputTokens":1286,"outputTokens":87,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[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,48,49,50,51,52,53,54,55,56,57],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":59,"time":1785591988431,"data":{"turn":1,"step":1,"callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","name":"glob","arguments":"{\"pattern\": \"*\", \"path\": \"tree\"}"}}
|
||||
{"type":"tool/result","seq":60,"time":1785591988476,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_1cLZjkCW0vxVw0e3xVfh3430"},"content":[{"type":"tool-result","toolCallId":"call_00_1cLZjkCW0vxVw0e3xVfh3430","content":[{"type":"text","text":"tree/archive/a.ts\ntree/docs/guide.md\ntree/src/index.ts\ntree/test/spec.ts\n\n(Showing 4 of 8 paths, sampled across 4 of the 6 top-level entries this pattern matched instead of taken in modification-time order. Narrow path to inspect a specific subtree. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"10284f88-4890-49ed-9a17-56edbd6bfaa7"},"meta":{"shape":"paths","paths":["tree/archive/a.ts","tree/docs/guide.md","tree/src/index.ts","tree/test/spec.ts"],"truncated":true,"total":8}},"sourceEventSeqs":[59],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":61,"time":1785591988476,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":62,"time":1785591988482,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":63,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"assistant/chunk","seq":64,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}
|
||||
{"type":"assistant/chunk","seq":65,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" glob"}}}
|
||||
{"type":"assistant/chunk","seq":66,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" result"}}}
|
||||
{"type":"assistant/chunk","seq":67,"time":1785591989939,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" shows"}}}
|
||||
{"type":"assistant/chunk","seq":68,"time":1785591989988,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}}
|
||||
{"type":"assistant/chunk","seq":69,"time":1785591990024,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}}
|
||||
{"type":"assistant/chunk","seq":70,"time":1785591990127,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sampled"}}}
|
||||
{"type":"assistant/chunk","seq":71,"time":1785591990128,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" -"}}}
|
||||
{"type":"assistant/chunk","seq":72,"time":1785591990128,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}}
|
||||
{"type":"assistant/chunk","seq":73,"time":1785591990128,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"4"}}}
|
||||
{"type":"assistant/chunk","seq":74,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}}
|
||||
{"type":"assistant/chunk","seq":75,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}}
|
||||
{"type":"assistant/chunk","seq":76,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"8"}}}
|
||||
{"type":"assistant/chunk","seq":77,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" paths"}}}
|
||||
{"type":"assistant/chunk","seq":78,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" across"}}}
|
||||
{"type":"assistant/chunk","seq":79,"time":1785591990454,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}}
|
||||
{"type":"assistant/chunk","seq":80,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"4"}}}
|
||||
{"type":"assistant/chunk","seq":81,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}}
|
||||
{"type":"assistant/chunk","seq":82,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}}
|
||||
{"type":"assistant/chunk","seq":83,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"6"}}}
|
||||
{"type":"assistant/chunk","seq":84,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" top"}}}
|
||||
{"type":"assistant/chunk","seq":85,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-level"}}}
|
||||
{"type":"assistant/chunk","seq":86,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" entries"}}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}
|
||||
{"type":"assistant/chunk","seq":88,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}}
|
||||
{"type":"assistant/chunk","seq":89,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}}
|
||||
{"type":"assistant/chunk","seq":90,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}
|
||||
{"type":"assistant/chunk","seq":91,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}}
|
||||
{"type":"assistant/chunk","seq":92,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}
|
||||
{"type":"assistant/chunk","seq":94,"time":1785591990455,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}}
|
||||
{"type":"assistant/chunk","seq":95,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"G"}}}
|
||||
{"type":"assistant/chunk","seq":96,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"LOB"}}}
|
||||
{"type":"assistant/chunk","seq":97,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_S"}}}
|
||||
{"type":"assistant/chunk","seq":98,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"AM"}}}
|
||||
{"type":"assistant/chunk","seq":99,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"PL"}}}
|
||||
{"type":"assistant/chunk","seq":100,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ED"}}}
|
||||
{"type":"assistant/chunk","seq":101,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}}
|
||||
{"type":"assistant/chunk","seq":102,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}}
|
||||
{"type":"assistant/chunk","seq":103,"time":1785591990456,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instructed"}}}
|
||||
{"type":"assistant/chunk","seq":104,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}
|
||||
{"type":"assistant/chunk","seq":105,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":106,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"G"}}}
|
||||
{"type":"assistant/chunk","seq":107,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"LOB"}}}
|
||||
{"type":"assistant/chunk","seq":108,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_S"}}}
|
||||
{"type":"assistant/chunk","seq":109,"time":1785591990470,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"AM"}}}
|
||||
{"type":"assistant/chunk","seq":110,"time":1785591990518,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"PL"}}}
|
||||
{"type":"assistant/chunk","seq":111,"time":1785591990518,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ED"}}}
|
||||
{"type":"assistant/chunk","seq":112,"time":1785591990526,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."}}}}
|
||||
{"type":"assistant/chunk","seq":113,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"GLOB_SAMPLED"}}}}
|
||||
{"type":"assistant/chunk","seq":114,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}}}}
|
||||
{"type":"assistant/chunk","seq":115,"time":1785591990527,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":116,"time":1785591990527,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The glob result shows it was sampled - 4 of 8 paths across 4 of 6 top-level entries. I need to reply with exactly \"GLOB_SAMPLED\" as instructed."},{"type":"text","text":"GLOB_SAMPLED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"dd3a9c28-43b2-4fdc-8089-1547309a71c0"},"usage":{"inputTokens":188,"outputTokens":48,"cacheReadTokens":1280,"reasoningTokens":41}},"sourceEventSeqs":[63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":117,"time":1785591990527,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":118,"time":1785591990528,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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-search/README.md
|
||||
README.md: 0152be017ae15fc83a3d5cb7df927f25d04d2d53
|
||||
README.zh.md: 69ce49f3ad1621021dc1d0938cdc07a900d1cda8
|
||||
README.md: 78ffa069e56da5fc987913acf761eb5c6ae15b1a
|
||||
README.zh.md: 42b123d5c47d8f48bc21b6f9bed4905372ca8625
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The **model-facing filesystem discovery tools**—`glob`, `grep`—are backed by the **packaged ripgrep binary** (`@vscode/ripgrep`), not by `ctx.fs` provider methods and not by a system `rg` install. Registration is unconditional: the binary ships inside the npm dependency, so there is no load-time availability probe. Each call spawns the binary through the `ctx.subprocess` seam with a fixed argv vector (model-controlled values are plain argv elements — no shell layer exists, so no quoting applies), parses the raw `rg` output, and returns a workdir-relative canonical value. The package injects `tools`, `systemPrompt`, and `subprocess`—deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional.
|
||||
The **model-facing filesystem discovery tools**—`glob`, `grep`—are backed by the **packaged ripgrep binary** (`@vscode/ripgrep`), not by `ctx.fs` provider methods and not by a system `rg` install. Registration is unconditional: the binary ships inside the npm dependency, so there is no load-time availability probe. Each call spawns the binary through the `ctx.subprocess` seam with a fixed argv vector (`--no-config` prepended so a host `RIPGREP_CONFIG_PATH` cannot inject a `--pre` preprocessor into the unconfined spawn; model-controlled values are plain argv elements — no shell layer exists, so no quoting applies), parses the raw `rg` output, and returns a workdir-relative canonical value. The package injects `tools`, `systemPrompt`, and `subprocess`—deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional.
|
||||
|
||||
```ts ignore-check
|
||||
// A deployment chooses how over-cap glob pages are selected.
|
||||
@@ -30,6 +30,8 @@ The binary ships with the package on every supported platform (macOS/Linux/Windo
|
||||
| `grepMaxLineBytes` | `2000` | Byte cap per matched-line preview; the cut preserves UTF-8 boundaries and is marked `(line truncated)`. |
|
||||
| `rawOutputMaxBytes` | `20000000` | Max complete raw `rg` stdout a search will parse (matches Claude Code's ripgrep raw buffer); larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. |
|
||||
| `timeoutMs` | `30000` | Cooperative tool-call budget attached to both tool definitions, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`; the subprocess seam's terminate escalation is the hard kill. |
|
||||
| `graceMs` | `3000` | Terminate-escalation grace period the subprocess seam grants past `timeoutMs` before the search fails as `SEARCH_ABORTED`. |
|
||||
| `stderrMaxBytes` | `65536` | Diagnostic-tail budget for `rg` stderr, captured through the subprocess seam's collect disposition; a lossy read keeps only the tail (marked `[stderr truncated]`). |
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -42,7 +44,7 @@ Routine budgets stay out of the model-facing schema (no `head_limit`/`offset`/`c
|
||||
|
||||
## Two budgets, two artifacts
|
||||
|
||||
Raw `rg` stdout is an internal transport detail. Each search requests a collect-mode stdout budget of `rawOutputMaxBytes` from the subprocess seam and parses only complete retained stdout; if the seam still reports a lossy read, the search fails with `SEARCH_RAW_OUTPUT_OVERFLOW` and tells the model to narrow the query. A successful `glob` keeps the displayed search root and every acquired path in `{ root, paths }`; when sampling is enabled, `root` lets the Native renderer group an explicit relative or absolute search path by entries beneath that root rather than by its workdir prefix. `grep` keeps every acquired `{ path, lineNumber, line }` in `{ matches }`. Inline item and per-line preview caps apply only in the Native renderer. For a direct surface call with more logical results than the inline cap, post-policy best-effort saves the complete formatted preview through `ctx.spillStore.saveText()` and replaces only presentation with the configured page plus locator. Nested Code dispatches skip that spill because their full canonical value does not enter model context. Missing/failed spill keeps the inline page and reports that the complete result could not be saved—never an `isError`.
|
||||
Raw `rg` stdout and stderr are internal transport details. Each search requests collect-mode budgets from the subprocess seam — complete stdout within `rawOutputMaxBytes` and a `stderrMaxBytes` diagnostic tail — with no spill files on either stream (the tool never reads a raw spill path). If the seam still reports a lossy stdout read, the search fails with `SEARCH_RAW_OUTPUT_OVERFLOW` and tells the model to narrow the query; a lossy stderr read only marks the diagnostic excerpt `[stderr truncated]`. A successful `glob` keeps the displayed search root and every acquired path in `{ root, paths }`; when sampling is enabled, `root` lets the Native renderer group an explicit relative or absolute search path by entries beneath that root rather than by its workdir prefix. `grep` keeps every acquired `{ path, lineNumber, line }` in `{ matches }`. Inline item and per-line preview caps apply only in the Native renderer. For a direct surface call with more logical results than the inline cap, post-policy best-effort saves the complete formatted preview through `ctx.spillStore.saveText()` and replaces only presentation with the configured page plus locator. Nested Code dispatches skip that spill because their full canonical value does not enter model context. Missing/failed spill keeps the inline page and reports that the complete result could not be saved—never an `isError`.
|
||||
|
||||
## Errors
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由 **打包的 ripgrep 二进制**(`@vscode/ripgrep`)支持,而不是由 `ctx.fs` 提供方方法或系统 `rg` 安装支持。注册是无条件的:二进制随 npm 依赖一起交付,因此没有加载期可用性探针。每次调用都通过 `ctx.subprocess` seam 以固定 argv 向量 spawn 该二进制(模型控制的值是普通 argv 元素——不存在 shell 层,因此无需引号),解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `subprocess`,有意**不**注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由 **打包的 ripgrep 二进制**(`@vscode/ripgrep`)支持,而不是由 `ctx.fs` 提供方方法或系统 `rg` 安装支持。注册是无条件的:二进制随 npm 依赖一起交付,因此没有加载期可用性探针。每次调用都通过 `ctx.subprocess` seam 以固定 argv 向量 spawn 该二进制(前缀 `--no-config`,使宿主的 `RIPGREP_CONFIG_PATH` 无法向不受约束的 spawn 注入 `--pre` 预处理器;模型控制的值是普通 argv 元素——不存在 shell 层,因此无需引号),解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `subprocess`,有意**不**注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
|
||||
```ts ignore-check
|
||||
// A deployment chooses how over-cap glob pages are selected.
|
||||
@@ -30,6 +30,8 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
|
||||
| `grepMaxLineBytes` | `2000` | 每条匹配行预览的字节上限;截断会保留 UTF-8 边界,并标记为 `(line truncated)`。 |
|
||||
| `rawOutputMaxBytes` | `20000000` | 搜索将解析的完整原始 `rg` stdout 上限(与 Claude Code 的 ripgrep 原始 buffer 相同);更大的原始输出以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败。 |
|
||||
| `timeoutMs` | `30000` | 附加到两个工具定义上的协作式工具调用预算,由 `@deepseek-ai/dsh-timeout-policy` 通过 `exec.signal` 强制执行;subprocess seam 的终止升级提供硬终止。 |
|
||||
| `graceMs` | `3000` | subprocess seam 在 `timeoutMs` 之外授予的终止升级宽限期;超过后搜索以 `SEARCH_ABORTED` 失败。 |
|
||||
| `stderrMaxBytes` | `65536` | `rg` stderr 的诊断尾部预算,经 subprocess seam 的 collect 形态捕获;lossy 读取只保留尾部(标记 `[stderr truncated]`)。 |
|
||||
|
||||
## 工具
|
||||
|
||||
@@ -42,7 +44,7 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
|
||||
|
||||
## 两类预算、两类产物
|
||||
|
||||
原始 `rg` stdout 是内部传输细节。每次搜索从 subprocess seam 请求 `rawOutputMaxBytes` 的 collect 模式 stdout 预算,且只解析完整保留的 stdout;如果 seam 仍报告 lossy 读取,搜索会以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败,并要求模型缩小查询。成功的 `glob` 在 `{ root, paths }` 中保留所显示的搜索根及所有已取得路径;启用采样时,借助 `root`,原生渲染器能以显式的相对或绝对搜索路径为根,按该根下的条目分组,而不是按其工作目录前缀分组。`grep` 保留所有已取得的 `{ path, lineNumber, line }`,并将其存入 `{ matches }`。内联条目和每行预览上限只应用于原生渲染器。直接接口调用的逻辑结果超过内联上限时,后置策略会尽力通过 `ctx.spillStore.saveText()` 保存完整格式化预览,并只把呈现替换为配置指定的页面与 locator。嵌套 Code 分派会跳过 spill,因为其完整规范值不会进入模型上下文。spill 缺失/失败时保留内联页面,并报告完整结果无法保存,绝不会成为 `isError`。
|
||||
原始 `rg` stdout 与 stderr 是内部传输细节。每次搜索从 subprocess seam 请求 collect 模式预算——`rawOutputMaxBytes` 内的完整 stdout 与 `stderrMaxBytes` 的诊断尾部——两条流都不产生 spill 文件(工具从不读取原始 spill 路径)。如果 seam 仍报告 lossy stdout 读取,搜索会以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败,并要求模型缩小查询;lossy stderr 读取只把诊断摘录标记为 `[stderr truncated]`。成功的 `glob` 在 `{ root, paths }` 中保留所显示的搜索根及所有已取得路径;启用采样时,借助 `root`,原生渲染器能以显式的相对或绝对搜索路径为根,按该根下的条目分组,而不是按其工作目录前缀分组。`grep` 保留所有已取得的 `{ path, lineNumber, line }`,并将其存入 `{ matches }`。内联条目和每行预览上限只应用于原生渲染器。直接接口调用的逻辑结果超过内联上限时,后置策略会尽力通过 `ctx.spillStore.saveText()` 保存完整格式化预览,并只把呈现替换为配置指定的页面与 locator。嵌套 Code 分派会跳过 spill,因为其完整规范值不会进入模型上下文。spill 缺失/失败时保留内联页面,并报告完整结果无法保存,绝不会成为 `isError`。
|
||||
|
||||
## 错误
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface GlobToolCaps {
|
||||
maxMetaBytes: number
|
||||
/** Cap on the complete raw `rg` stdout the tool will parse. */
|
||||
rawOutputMaxBytes: number
|
||||
/** Terminate-escalation grace period (ms) for the search process. */
|
||||
graceMs: number
|
||||
/** Cap on the retained stderr diagnostic tail. */
|
||||
stderrMaxBytes: number
|
||||
/** Cooperative tool-call budget (ms) attached as `ToolDefinition.timeoutMs`. */
|
||||
timeoutMs: number
|
||||
}
|
||||
@@ -337,7 +341,7 @@ export function applyGlobTool(ctx: Context, caps: GlobToolCaps): void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const input = parseGlobArgs(args)
|
||||
const run = await runRipgrep(ctx, exec, 'glob', buildGlobCommand(input), caps.rawOutputMaxBytes)
|
||||
const run = await runRipgrep(ctx, exec, 'glob', buildGlobCommand(input), caps.rawOutputMaxBytes, caps.graceMs, caps.stderrMaxBytes)
|
||||
const root = input.path === undefined ? '.' : toWorkdirRelative(input.path, run.workdir)
|
||||
if (run.noMatches) return { root, paths: [] }
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ export interface GrepToolCaps {
|
||||
maxMetaBytes: number
|
||||
/** Cap on the complete raw `rg` stdout the tool will parse. */
|
||||
rawOutputMaxBytes: number
|
||||
/** Terminate-escalation grace period (ms) for the search process. */
|
||||
graceMs: number
|
||||
/** Cap on the retained stderr diagnostic tail. */
|
||||
stderrMaxBytes: number
|
||||
/** Cooperative tool-call budget (ms) attached as `ToolDefinition.timeoutMs`. */
|
||||
timeoutMs: number
|
||||
}
|
||||
@@ -265,7 +269,7 @@ export function presentGrepResult(
|
||||
* Register the `grep` tool and its system-prompt guidance.
|
||||
*
|
||||
* @param ctx - the plugin context; registrations are effects scoped to it, and
|
||||
* execution uses its `bash` service.
|
||||
* execution uses its `subprocess` service.
|
||||
* @param caps - the deployment's resolved grep caps (plugin config after defaulting).
|
||||
*/
|
||||
export function applyGrepTool(ctx: Context, caps: GrepToolCaps): void {
|
||||
@@ -315,7 +319,7 @@ export function applyGrepTool(ctx: Context, caps: GrepToolCaps): void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const input = parseGrepArgs(args)
|
||||
const run = await runRipgrep(ctx, exec, 'grep', buildGrepCommand(input), caps.rawOutputMaxBytes)
|
||||
const run = await runRipgrep(ctx, exec, 'grep', buildGrepCommand(input), caps.rawOutputMaxBytes, caps.graceMs, caps.stderrMaxBytes)
|
||||
if (run.noMatches) return { matches: [] }
|
||||
|
||||
const all: GrepMatch[] = []
|
||||
|
||||
@@ -30,7 +30,7 @@ import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { GLOB_MAX_RESULTS, applyGlobTool } from './glob.ts'
|
||||
import { GREP_MAX_LINE_BYTES, GREP_MAX_MATCHES, applyGrepTool } from './grep.ts'
|
||||
import { RAW_OUTPUT_MAX_BYTES, SEARCH_META_MAX_BYTES, SEARCH_TIMEOUT_MS } from './search-core.ts'
|
||||
import { RAW_OUTPUT_MAX_BYTES, SEARCH_GRACE_MS, SEARCH_META_MAX_BYTES, SEARCH_STDERR_MAX_BYTES, SEARCH_TIMEOUT_MS } from './search-core.ts'
|
||||
|
||||
export { GLOB_MAX_RESULTS, GLOB_VCS_EXCLUDES, applyGlobTool, buildGlobCommand, formatGlobOutput, parseGlobArgs, presentGlobCall, presentGlobResult, sampleAcrossTopLevel } from './glob.ts'
|
||||
export type { GlobInput, GlobSample, GlobToolCaps } from './glob.ts'
|
||||
@@ -49,7 +49,9 @@ export {
|
||||
export type { GrepInput, GrepToolCaps } from './grep.ts'
|
||||
export {
|
||||
RAW_OUTPUT_MAX_BYTES,
|
||||
SEARCH_GRACE_MS,
|
||||
SEARCH_META_MAX_BYTES,
|
||||
SEARCH_STDERR_MAX_BYTES,
|
||||
SEARCH_TIMEOUT_MS,
|
||||
SearchError,
|
||||
previewLine,
|
||||
@@ -58,7 +60,6 @@ export {
|
||||
trySaveFormattedResult,
|
||||
} from './search-core.ts'
|
||||
export type { GrepMatch, RipgrepRun, SearchErrorCode } from './search-core.ts'
|
||||
export { singleQuote } from './shell-quote.ts'
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = 'tool-fs-search'
|
||||
@@ -80,6 +81,10 @@ export interface Config {
|
||||
searchMetaMaxBytes?: number
|
||||
/** Max complete raw `rg` stdout bytes a search will parse; larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. */
|
||||
rawOutputMaxBytes?: number
|
||||
/** Terminate-escalation grace period (ms) for one search process, handed to the subprocess seam. */
|
||||
graceMs?: number
|
||||
/** Max bytes retained for one search's stderr diagnostic tail (never surfaced to the model). */
|
||||
stderrMaxBytes?: number
|
||||
/** Cooperative tool-call timeout budget (ms) on both tools, enforced by `@deepseek-ai/dsh-timeout-policy` through `exec.signal`. */
|
||||
timeoutMs?: number
|
||||
}
|
||||
@@ -91,6 +96,8 @@ export const Config: z<Config> = z.object({
|
||||
grepMaxLineBytes: z.number().default(GREP_MAX_LINE_BYTES),
|
||||
searchMetaMaxBytes: z.number().default(SEARCH_META_MAX_BYTES),
|
||||
rawOutputMaxBytes: z.number().default(RAW_OUTPUT_MAX_BYTES),
|
||||
graceMs: z.number().default(SEARCH_GRACE_MS),
|
||||
stderrMaxBytes: z.number().default(SEARCH_STDERR_MAX_BYTES),
|
||||
timeoutMs: z.number().default(SEARCH_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
@@ -121,12 +128,16 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
assertPositiveInteger('grepMaxLineBytes', resolved.grepMaxLineBytes)
|
||||
assertPositiveInteger('searchMetaMaxBytes', resolved.searchMetaMaxBytes)
|
||||
assertPositiveInteger('rawOutputMaxBytes', resolved.rawOutputMaxBytes)
|
||||
assertPositiveInteger('graceMs', resolved.graceMs)
|
||||
assertPositiveInteger('stderrMaxBytes', resolved.stderrMaxBytes)
|
||||
assertPositiveInteger('timeoutMs', resolved.timeoutMs)
|
||||
applyGlobTool(ctx, {
|
||||
sampleOverCapGlobResults: resolved.sampleOverCapGlobResults,
|
||||
maxResults: resolved.globMaxResults,
|
||||
maxMetaBytes: resolved.searchMetaMaxBytes,
|
||||
rawOutputMaxBytes: resolved.rawOutputMaxBytes,
|
||||
graceMs: resolved.graceMs,
|
||||
stderrMaxBytes: resolved.stderrMaxBytes,
|
||||
timeoutMs: resolved.timeoutMs,
|
||||
})
|
||||
applyGrepTool(ctx, {
|
||||
@@ -134,6 +145,8 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
maxLineBytes: resolved.grepMaxLineBytes,
|
||||
maxMetaBytes: resolved.searchMetaMaxBytes,
|
||||
rawOutputMaxBytes: resolved.rawOutputMaxBytes,
|
||||
graceMs: resolved.graceMs,
|
||||
stderrMaxBytes: resolved.stderrMaxBytes,
|
||||
timeoutMs: resolved.timeoutMs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,15 +44,13 @@ export const SEARCH_TIMEOUT_MS = 30_000
|
||||
|
||||
/**
|
||||
* Default cap in bytes on the retained stderr tail of one search run — a
|
||||
* diagnostic excerpt only (the tool never reads `stderr.spillPath`).
|
||||
* diagnostic excerpt only (the tool never reads a stderr spill path, and the
|
||||
* collect disposition requests none).
|
||||
*/
|
||||
const SEARCH_STDERR_MAX_BYTES = 64 * 1024
|
||||
|
||||
/** Default whole-stream spill cap for search output (the subprocess seam requires an explicit budget). */
|
||||
const SEARCH_SPILL_MAX_BYTES = 64 * 1024 * 1024
|
||||
export const SEARCH_STDERR_MAX_BYTES = 64 * 1024
|
||||
|
||||
/** Default terminate grace period for a search process (ms). */
|
||||
const SEARCH_GRACE_MS = 3_000
|
||||
export const SEARCH_GRACE_MS = 3_000
|
||||
|
||||
/**
|
||||
* Default cap in bytes on one search's serialized `presentationMeta` (the
|
||||
@@ -110,7 +108,7 @@ export interface RipgrepRun {
|
||||
|
||||
/**
|
||||
* The retained stderr tail as a diagnostic excerpt, with a truncation note when
|
||||
* the subprocess seam dropped bytes (the tool never reads `stderr.spillPath`).
|
||||
* the subprocess seam dropped bytes.
|
||||
*/
|
||||
function stderrExcerpt(stderrText: string, truncated: boolean): string {
|
||||
const text = stderrText.trim()
|
||||
@@ -118,15 +116,16 @@ function stderrExcerpt(stderrText: string, truncated: boolean): string {
|
||||
return truncated ? `${text} [stderr truncated]` : text
|
||||
}
|
||||
|
||||
/** Classify a nonzero-exit `rg` run into the search error vocabulary (invalid pattern vs missing `rg` vs everything else). */
|
||||
/**
|
||||
* Classify a nonzero-exit `rg` run into the search error vocabulary. There is
|
||||
* no shell layer, so an exit 127 or shell "command not found" text cannot
|
||||
* occur — a launch failure rejects at spawn (see {@link runRipgrep}).
|
||||
*/
|
||||
function classifyRunFailure(toolName: string, exitCode: number, stderrText: string, stderrTruncated: boolean): SearchError {
|
||||
const stderr = stderrExcerpt(stderrText, stderrTruncated)
|
||||
if (/regex parse error|error parsing glob/i.test(stderr)) {
|
||||
return new SearchError(`${toolName} pattern rejected by ripgrep: ${stderr}`, 'SEARCH_INVALID_PATTERN')
|
||||
}
|
||||
if (exitCode === 127 || /command not found/i.test(stderr)) {
|
||||
return new SearchError(`${toolName} requires ripgrep (rg) to launch${stderr.length > 0 ? `: ${stderr}` : ''}`, 'SEARCH_FAILED')
|
||||
}
|
||||
return new SearchError(`${toolName} search failed (exit ${exitCode})${stderr.length > 0 ? `: ${stderr}` : ''}`, 'SEARCH_FAILED')
|
||||
}
|
||||
|
||||
@@ -163,6 +162,13 @@ function completeStdout(toolName: string, stdout: SubprocessOutputRead, rawOutpu
|
||||
* (`@deepseek-ai/dsh-timeout-policy`) and caller cancellation terminate the
|
||||
* process tree.
|
||||
*
|
||||
* The spawn is unconfined (a plain `ctx.subprocess` call), so `--no-config`
|
||||
* is prepended: a host `RIPGREP_CONFIG_PATH` (or `rg.conf` next to the
|
||||
* binary) can otherwise inject `--pre` and make ripgrep execute an arbitrary
|
||||
* preprocessor for every matched file. The collect dispositions are the
|
||||
* seam's diagnostic-tail shape (no spill files): the tools never read a raw
|
||||
* spill path, and truncated stdout fails as `SEARCH_RAW_OUTPUT_OVERFLOW`.
|
||||
*
|
||||
* Exit semantics are tool-owned: exit 0 is success with results, exit 1 is
|
||||
* success with zero results (`noMatches`), anything else throws a
|
||||
* {@link SearchError} (abort/timeout → `SEARCH_ABORTED`, invalid pattern →
|
||||
@@ -176,6 +182,8 @@ function completeStdout(toolName: string, stdout: SubprocessOutputRead, rawOutpu
|
||||
* @param toolName - `glob` or `grep`, used in error messages.
|
||||
* @param argv - the ripgrep arguments (every model value an unquoted argv element; no shell layer exists).
|
||||
* @param rawOutputMaxBytes - cap on the complete raw stdout the tool will parse.
|
||||
* @param graceMs - the seam's terminate-escalation grace period.
|
||||
* @param stderrMaxBytes - cap on the retained stderr diagnostic tail.
|
||||
* @returns the complete stdout, the zero-result flag, and the resolved workdir.
|
||||
*/
|
||||
export async function runRipgrep(
|
||||
@@ -184,6 +192,8 @@ export async function runRipgrep(
|
||||
toolName: string,
|
||||
argv: readonly string[],
|
||||
rawOutputMaxBytes: number,
|
||||
graceMs: number,
|
||||
stderrMaxBytes: number,
|
||||
): Promise<RipgrepRun> {
|
||||
if (exec.signal.aborted) {
|
||||
throw new SearchError(`${toolName} was aborted before completion (tool timeout or caller cancellation)`, 'SEARCH_ABORTED')
|
||||
@@ -191,16 +201,16 @@ export async function runRipgrep(
|
||||
const cwd = exec.agent?.session.header.cwd
|
||||
const workdir = cwd ?? process.cwd()
|
||||
const collect = (maxBytes: number): SubprocessCollect =>
|
||||
({ maxBytes, spill: { maxBytes: SEARCH_SPILL_MAX_BYTES } })
|
||||
({ maxBytes })
|
||||
const handle = ctx.subprocess.spawn({
|
||||
argv: [rgPath, ...argv],
|
||||
argv: [rgPath, '--no-config', ...argv],
|
||||
cwd: workdir,
|
||||
stdio: {
|
||||
stdin: 'ignore',
|
||||
stdout: collect(rawOutputMaxBytes),
|
||||
stderr: collect(SEARCH_STDERR_MAX_BYTES),
|
||||
stderr: collect(stderrMaxBytes),
|
||||
},
|
||||
graceMs: SEARCH_GRACE_MS,
|
||||
graceMs,
|
||||
signal: exec.signal,
|
||||
} satisfies SubprocessSpawnSpec)
|
||||
let outcome: SubprocessOutcome
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* POSIX single-quoting helper retained for compatibility with older
|
||||
* deployments and tests. The current `glob`/`grep` command builders spawn the
|
||||
* packaged ripgrep binary with a plain argv vector — no shell layer exists —
|
||||
* so no quoting is involved; this module is kept because its export is part
|
||||
* of the package surface.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-tool-fs-search/shell-quote
|
||||
*/
|
||||
|
||||
/**
|
||||
* POSIX single-quote a string for safe use as ONE shell word. Wraps the value
|
||||
* in single quotes and rewrites every embedded single quote as `'\''` (close
|
||||
* quote, an escaped literal quote, reopen quote). Inside single quotes the shell
|
||||
* treats every other byte literally — spaces, newlines, `$`, backticks, `;`,
|
||||
* `|`, `&`, glob metacharacters, and a leading `-` are all inert — so the result
|
||||
* is a single, injection-safe argument regardless of the input.
|
||||
*
|
||||
* @param value - the raw, possibly model-controlled string to quote.
|
||||
* @returns the value wrapped as one safe single-quoted shell word.
|
||||
*/
|
||||
export function singleQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\\''")}'`
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Unit tests for the shell-quoting safety boundary, plus a REAL round-trip:
|
||||
* every adversarial value, quoted, must survive `bash -c "printf '%s' <quoted>"`
|
||||
* byte-for-byte — proving the quoting is inert in an actual shell, not just
|
||||
* against a mental model of one.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { singleQuote } from '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
/** Adversarial values a model could pass as pattern / path / include. */
|
||||
const HOSTILE: readonly string[] = [
|
||||
'plain',
|
||||
'with spaces',
|
||||
"it's got 'quotes'",
|
||||
'"double quoted"',
|
||||
'$(rm -rf /tmp/nope)',
|
||||
'`touch /tmp/nope`',
|
||||
'$HOME and ${PATH}',
|
||||
'semi;colon && chain || pipe | bg &',
|
||||
'newline\nin the middle',
|
||||
'-leading-dash',
|
||||
'--leading-double-dash',
|
||||
'*?[a-z]{x,y}',
|
||||
'!bang',
|
||||
'\\backslash\\',
|
||||
'~tilde',
|
||||
'# not a comment',
|
||||
'>redirect <input 2>&1',
|
||||
]
|
||||
|
||||
describe('singleQuote', () => {
|
||||
it('wraps a plain value in single quotes', () => {
|
||||
expect(singleQuote('abc')).toBe("'abc'")
|
||||
})
|
||||
|
||||
it("rewrites embedded single quotes as '\\''", () => {
|
||||
expect(singleQuote("a'b")).toBe("'a'\\''b'")
|
||||
expect(singleQuote("''")).toBe("''\\'''\\'''")
|
||||
})
|
||||
|
||||
it.each(HOSTILE.map(value => [JSON.stringify(value), value] as const))(
|
||||
'round-trips %s through a real bash -c unchanged',
|
||||
(_label, value) => {
|
||||
const result = spawnSync('bash', ['-c', `printf '%s' ${singleQuote(value)}`], { encoding: 'utf8' })
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toBe(value)
|
||||
},
|
||||
)
|
||||
|
||||
it('a quoted command substitution does not execute (the world stays untouched)', () => {
|
||||
const canary = `/tmp/dsh-quote-canary-${process.pid}`
|
||||
const result = spawnSync('bash', ['-c', `printf '%s' ${singleQuote(`$(touch ${canary})`)}`], { encoding: 'utf8' })
|
||||
expect(result.stdout).toBe(`$(touch ${canary})`)
|
||||
// The canary file must NOT exist — the substitution stayed literal.
|
||||
expect(spawnSync('test', ['-e', canary]).status).not.toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -291,6 +291,8 @@ describe('config validation', () => {
|
||||
['grepMaxMatches', { grepMaxMatches: -1 }],
|
||||
['grepMaxLineBytes', { grepMaxLineBytes: 1.5 }],
|
||||
['rawOutputMaxBytes', { rawOutputMaxBytes: 0 }],
|
||||
['graceMs', { graceMs: 0 }],
|
||||
['stderrMaxBytes', { stderrMaxBytes: -1 }],
|
||||
['timeoutMs', { timeoutMs: -100 }],
|
||||
] as const)('rejects a non-positive or fractional %s at load', async (name, config) => {
|
||||
const ctx = new Context()
|
||||
@@ -372,16 +374,30 @@ describe('workdir derivation and signal forwarding', () => {
|
||||
expect(subprocess.spawns[1]?.cwd).toBe(process.cwd())
|
||||
})
|
||||
|
||||
it('spawns the packaged ripgrep binary with the fixed argv and budgeted collect streams', async () => {
|
||||
const { ctx, subprocess } = await setup({ config: { rawOutputMaxBytes: 1234 } })
|
||||
it('spawns the packaged ripgrep binary with --no-config, the fixed argv, and budgeted collect streams', async () => {
|
||||
const { ctx, subprocess } = await setup({
|
||||
config: { rawOutputMaxBytes: 1234, graceMs: 5000, stderrMaxBytes: 4096 },
|
||||
})
|
||||
subprocess.handler = () => runResult('', { exitCode: 1 })
|
||||
await call(ctx, 'grep', { pattern: 'needle' })
|
||||
const spec = subprocess.spawns[0]
|
||||
expect(spec?.argv[0]).toBe(rgPath)
|
||||
expect(spec?.argv).toEqual([rgPath, '--json', '--regexp=needle'])
|
||||
// --no-config keeps a host RIPGREP_CONFIG_PATH from injecting a
|
||||
// preprocessor into this unconfined spawn.
|
||||
expect(spec?.argv).toEqual([rgPath, '--no-config', '--json', '--regexp=needle'])
|
||||
expect(spec?.stdio.stdin).toBe('ignore')
|
||||
// stdout gets the tool's parse budget; stderr is a diagnostic excerpt.
|
||||
// stdout gets the tool's parse budget; stderr is a diagnostic excerpt;
|
||||
// both are the seam's diagnostic-tail shape (no spill files requested).
|
||||
expect((spec?.stdio.stdout as { maxBytes: number }).maxBytes).toBe(1234)
|
||||
expect((spec?.stdio.stderr as { maxBytes: number }).maxBytes).toBe(4096)
|
||||
expect(spec?.graceMs).toBe(5_000)
|
||||
})
|
||||
|
||||
it('defaults the stderr tail budget and grace period when the config omits them', async () => {
|
||||
const { ctx, subprocess } = await setup()
|
||||
subprocess.handler = () => runResult('', { exitCode: 1 })
|
||||
await call(ctx, 'grep', { pattern: 'needle' })
|
||||
const spec = subprocess.spawns[0]
|
||||
expect((spec?.stdio.stderr as { maxBytes: number }).maxBytes).toBe(64 * 1024)
|
||||
expect(spec?.graceMs).toBe(3_000)
|
||||
})
|
||||
|
||||
@@ -430,7 +446,7 @@ describe('workdir derivation and signal forwarding', () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
const exec = { signal: controller.signal, name: 'glob', callId: CallId('direct-pre-abort') } as unknown as ToolExecution
|
||||
await expect(runRipgrep(ctx, exec, 'glob', ['--files'], 1_000_000)).rejects
|
||||
await expect(runRipgrep(ctx, exec, 'glob', ['--files'], 1_000_000, 3_000, 64 * 1024)).rejects
|
||||
.toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
|
||||
})
|
||||
|
||||
@@ -489,20 +505,6 @@ describe('exit semantics and failure classification', () => {
|
||||
expect(result.error).toMatchObject({ info: { code: 'SEARCH_INVALID_PATTERN' } })
|
||||
})
|
||||
|
||||
it('a failed ripgrep launch classifies as SEARCH_FAILED naming ripgrep', async () => {
|
||||
const { ctx, subprocess } = await setup()
|
||||
subprocess.handler = () => runResult('', { exitCode: 127, stderr: { text: 'sh: rg: command not found' } })
|
||||
const result = await call(ctx, 'glob', { pattern: '*' })
|
||||
expect(result.error).toMatchObject({ info: { code: 'SEARCH_FAILED' } })
|
||||
expect(text(result)).toContain('requires ripgrep (rg)')
|
||||
// The same classification holds from either evidence alone: the 127 exit
|
||||
// with silent stderr, or a shell's command-not-found text on another exit.
|
||||
subprocess.handler = () => runResult('', { exitCode: 127 })
|
||||
expect(text(await call(ctx, 'glob', { pattern: '*' }))).toContain('requires ripgrep (rg)')
|
||||
subprocess.handler = () => runResult('', { exitCode: 2, stderr: { text: 'sh: rg: command not found' } })
|
||||
expect(text(await call(ctx, 'grep', { pattern: 'x' }))).toContain('requires ripgrep (rg)')
|
||||
})
|
||||
|
||||
it('other nonzero exits are SEARCH_FAILED carrying the stderr excerpt', async () => {
|
||||
const { ctx, subprocess } = await setup()
|
||||
subprocess.handler = () => runResult('', { exitCode: 2, stderr: { text: 'rg: missing.dir: IO error: no such file or directory' } })
|
||||
@@ -682,7 +684,7 @@ describe('glob results', () => {
|
||||
subprocess.handler = () => runResult('sub/a.ts\n')
|
||||
const result = await call(ctx, 'glob', { pattern: '*.ts', path: 'sub' })
|
||||
expect(result.isError).toBe(false)
|
||||
expect(subprocess.spawns[0]?.argv).toEqual([rgPath, '--files', '--glob=*.ts', '--sort=modified', '--no-ignore', '--hidden',
|
||||
expect(subprocess.spawns[0]?.argv).toEqual([rgPath, '--no-config', '--files', '--glob=*.ts', '--sort=modified', '--no-ignore', '--hidden',
|
||||
'--glob=!**/.git', '--glob=!**/.git/**',
|
||||
'--glob=!**/.svn', '--glob=!**/.svn/**',
|
||||
'--glob=!**/.hg', '--glob=!**/.hg/**',
|
||||
|
||||
Reference in New Issue
Block a user