@namzu/sdk 59.0.0 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +4 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +90 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +264 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +32 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/public-runtime.ts +21 -2
- package/src/public-types.ts +1 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +92 -10
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/plugin/index.ts +8 -0
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +36 -2
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { walkFilesLocally } from '../../sandbox/file-walk.js'
|
|
4
|
-
import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
|
|
4
|
+
import type { SandboxWalkFilesOptions, SandboxWalkSummary } from '../../types/sandbox/index.js'
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
6
|
import { resolveWithin, resolveWithinAny, resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
7
7
|
import { relativePosix, resolveWithinPosix } from '../posix-path.js'
|
|
8
|
+
import {
|
|
9
|
+
SEARCH_BUDGET_MS,
|
|
10
|
+
hostDisplayPath,
|
|
11
|
+
ignoredHint,
|
|
12
|
+
isEntryLimit,
|
|
13
|
+
isReadDenied,
|
|
14
|
+
openSearchBudget,
|
|
15
|
+
plural,
|
|
16
|
+
quoteControlPath,
|
|
17
|
+
stoppedNotice,
|
|
18
|
+
} from './search-support.js'
|
|
8
19
|
|
|
9
20
|
const MAX_GLOB_RESULTS = 500
|
|
10
|
-
const MAX_VISITED_ENTRIES = 20_000
|
|
11
21
|
|
|
12
22
|
const inputSchema = z.object({
|
|
13
23
|
pattern: z
|
|
@@ -27,7 +37,13 @@ const inputSchema = z.object({
|
|
|
27
37
|
.boolean()
|
|
28
38
|
.optional()
|
|
29
39
|
.describe(
|
|
30
|
-
'Include hidden files and directories in wildcard matches. Default:
|
|
40
|
+
'Include hidden files and directories in wildcard matches. Default: true. Version-control directories are always skipped.',
|
|
41
|
+
),
|
|
42
|
+
include_ignored: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(
|
|
46
|
+
'Also return files that .gitignore and .ignore rules exclude, such as build output and dependencies. Default: false.',
|
|
31
47
|
),
|
|
32
48
|
})
|
|
33
49
|
|
|
@@ -49,16 +65,10 @@ function relativePattern(pattern: string, root: string, sandboxed: boolean): str
|
|
|
49
65
|
return result || '.'
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
/** Keep unusual filenames unambiguous without changing the returned path data. */
|
|
53
|
-
function displayPath(path: string): string {
|
|
54
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: quote filenames containing control bytes instead of creating fake result lines.
|
|
55
|
-
return /[\x00-\x1f\x7f]/.test(path) ? JSON.stringify(path) : path
|
|
56
|
-
}
|
|
57
|
-
|
|
58
68
|
export const GlobTool = defineTool({
|
|
59
69
|
name: 'glob',
|
|
60
70
|
description:
|
|
61
|
-
'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Returns at most 500 paths and
|
|
71
|
+
'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Returns at most 500 paths and says when a result is limited or a search stopped early. Does not follow symlinks.',
|
|
62
72
|
inputSchema,
|
|
63
73
|
category: 'filesystem',
|
|
64
74
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -102,57 +112,94 @@ export const GlobTool = defineTool({
|
|
|
102
112
|
'This sandbox does not support bounded file discovery (Sandbox.walkFiles). Update its adapter or use an available directory-listing command inside the sandbox.',
|
|
103
113
|
}
|
|
104
114
|
}
|
|
115
|
+
const budget = openSearchBudget(context.abortSignal)
|
|
116
|
+
let summary: SandboxWalkSummary | undefined
|
|
105
117
|
const options: SandboxWalkFilesOptions = {
|
|
106
118
|
pattern,
|
|
107
|
-
signal:
|
|
119
|
+
signal: budget.signal,
|
|
108
120
|
maxEntries: MAX_GLOB_RESULTS + 1,
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
includeHidden: input.include_hidden ?? true,
|
|
122
|
+
respectIgnore: input.include_ignored !== true,
|
|
123
|
+
onSummary: (value) => {
|
|
124
|
+
summary = value
|
|
125
|
+
},
|
|
111
126
|
}
|
|
112
127
|
const entries = walkSandbox ? walkSandbox(root, options) : walkFilesLocally(root, options)
|
|
113
128
|
const files: string[] = []
|
|
114
|
-
let
|
|
129
|
+
let capped = false
|
|
130
|
+
let timedOut = false
|
|
131
|
+
let legacyEntryLimit = false
|
|
115
132
|
let failure: string | undefined
|
|
116
133
|
try {
|
|
117
134
|
for await (const entry of entries) {
|
|
118
|
-
|
|
135
|
+
budget.signal.throwIfAborted()
|
|
119
136
|
const absolute = sandbox
|
|
120
137
|
? resolveWithinPosix(root, entry.path)
|
|
121
138
|
: resolveWithin(root, entry.path)
|
|
139
|
+
if (isReadDenied(context, absolute, sandbox?.rootDir)) continue
|
|
122
140
|
if (files.length === MAX_GLOB_RESULTS) {
|
|
123
|
-
|
|
141
|
+
capped = true
|
|
124
142
|
break
|
|
125
143
|
}
|
|
126
144
|
// Readers resolve relative input against the original working path,
|
|
127
145
|
// which may be a symlink. Project back through the selected alias
|
|
128
146
|
// before relativizing so added directories remain reachable.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
files.push(
|
|
148
|
+
sandbox
|
|
149
|
+
? relativePosix(sandbox.rootDir, absolute)
|
|
150
|
+
: hostDisplayPath(
|
|
151
|
+
context.workingDirectory,
|
|
152
|
+
join(lexicalRoot, relative(root, absolute)),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
133
155
|
}
|
|
134
156
|
} catch (error) {
|
|
135
157
|
if (context.abortSignal?.aborted) throw error
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
if (budget.expired()) timedOut = true
|
|
159
|
+
else if (isEntryLimit(error)) legacyEntryLimit = true
|
|
160
|
+
else failure = error instanceof Error ? error.message : String(error)
|
|
161
|
+
} finally {
|
|
162
|
+
budget.close()
|
|
138
163
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
const endState = summary as SandboxWalkSummary | undefined
|
|
165
|
+
const entryLimit = endState?.stopped === 'entry-limit' || legacyEntryLimit
|
|
166
|
+
const incomplete = timedOut || entryLimit
|
|
167
|
+
if (timedOut && files.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
output: '',
|
|
171
|
+
error: `Search timed out after ${SEARCH_BUDGET_MS / 1000} seconds with no matches. Try a more specific path or pattern.`,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const notices = [
|
|
175
|
+
...(capped
|
|
176
|
+
? [
|
|
177
|
+
`(Showing the first ${plural(MAX_GLOB_RESULTS, 'matching file')}; more match. Narrow the pattern or path to see the rest.)`,
|
|
178
|
+
]
|
|
179
|
+
: []),
|
|
180
|
+
...(incomplete ? [stoppedNotice(timedOut ? 'time' : 'entries', endState?.visited)] : []),
|
|
181
|
+
...(failure ? [`[Search incomplete: ${failure}]`] : []),
|
|
182
|
+
]
|
|
183
|
+
const ignored = endState?.ignoredDirectories ?? 0
|
|
184
|
+
const hint = ignored > 0 ? `\n${ignoredHint(ignored)}` : ''
|
|
185
|
+
const empty = `No files found matching pattern "${input.pattern}" in ${root}${hint}`
|
|
144
186
|
return {
|
|
145
187
|
success: failure === undefined,
|
|
146
|
-
output:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
188
|
+
output: [...files.map(quoteControlPath), ...notices].join('\n') || empty,
|
|
189
|
+
data: {
|
|
190
|
+
count: files.length,
|
|
191
|
+
files,
|
|
192
|
+
truncated: capped || incomplete,
|
|
193
|
+
...(sandbox ? { sandboxed: true } : {}),
|
|
194
|
+
},
|
|
150
195
|
facts: {
|
|
151
196
|
kind: 'list',
|
|
152
197
|
pattern: input.pattern,
|
|
153
198
|
...(input.path !== undefined ? { path: input.path } : {}),
|
|
154
199
|
count: files.length,
|
|
155
|
-
truncated,
|
|
200
|
+
truncated: capped || incomplete,
|
|
201
|
+
...(capped ? { limited: true } : {}),
|
|
202
|
+
...(incomplete ? { incomplete: true } : {}),
|
|
156
203
|
},
|
|
157
204
|
...(failure ? { error: failure } : {}),
|
|
158
205
|
}
|
|
@@ -2,7 +2,7 @@ import { stat } from 'node:fs/promises'
|
|
|
2
2
|
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { walkFilesLocally } from '../../sandbox/file-walk.js'
|
|
5
|
-
import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
|
|
5
|
+
import type { SandboxWalkFilesOptions, SandboxWalkSummary } from '../../types/sandbox/index.js'
|
|
6
6
|
import type { ToolContext } from '../../types/tool/index.js'
|
|
7
7
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
8
8
|
import { defineTool } from '../defineTool.js'
|
|
@@ -15,6 +15,17 @@ import {
|
|
|
15
15
|
} from '../paths.js'
|
|
16
16
|
import { relativePosix, resolveWithinPosix } from '../posix-path.js'
|
|
17
17
|
import { readBoundedChunks, readBoundedFile } from './bounded-file-read.js'
|
|
18
|
+
import {
|
|
19
|
+
MAX_FILES_SEARCHED,
|
|
20
|
+
SEARCH_BUDGET_MS,
|
|
21
|
+
hostDisplayPath,
|
|
22
|
+
ignoredHint,
|
|
23
|
+
isEntryLimit,
|
|
24
|
+
isReadDenied,
|
|
25
|
+
openSearchBudget,
|
|
26
|
+
plural,
|
|
27
|
+
stoppedNotice,
|
|
28
|
+
} from './search-support.js'
|
|
18
29
|
|
|
19
30
|
/** A remote read has no signal parameter; stop consuming it when the turn ends. */
|
|
20
31
|
async function readWithSignal(read: () => Promise<Buffer>, signal?: AbortSignal): Promise<Buffer> {
|
|
@@ -166,6 +177,18 @@ const inputSchema = z.object({
|
|
|
166
177
|
.describe(
|
|
167
178
|
'Only search files of this kind, e.g. "js", "ts", "py", "rust", "go", "java", "md". Narrows what include selects.',
|
|
168
179
|
),
|
|
180
|
+
include_hidden: z
|
|
181
|
+
.boolean()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe(
|
|
184
|
+
'Search hidden files and directories (names starting with a dot). Default: true. Version-control directories are always skipped.',
|
|
185
|
+
),
|
|
186
|
+
include_ignored: z
|
|
187
|
+
.boolean()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe(
|
|
190
|
+
'Also search files that .gitignore and .ignore rules exclude, such as build output and dependencies. Default: false.',
|
|
191
|
+
),
|
|
169
192
|
max_results: z.coerce
|
|
170
193
|
.number()
|
|
171
194
|
.int()
|
|
@@ -176,7 +199,6 @@ const inputSchema = z.object({
|
|
|
176
199
|
|
|
177
200
|
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB — skip binaries/large files
|
|
178
201
|
const BINARY_CHECK_BYTES = 512
|
|
179
|
-
const MAX_VISITED_ENTRIES = 20_000
|
|
180
202
|
|
|
181
203
|
function isBinaryContent(buffer: Buffer): boolean {
|
|
182
204
|
const check = buffer.subarray(0, BINARY_CHECK_BYTES)
|
|
@@ -189,7 +211,7 @@ function isBinaryContent(buffer: Buffer): boolean {
|
|
|
189
211
|
export const GrepTool = defineTool({
|
|
190
212
|
name: 'grep',
|
|
191
213
|
description:
|
|
192
|
-
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches
|
|
214
|
+
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches hidden files unless include_hidden is false. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Says when a result is limited or a search stopped early. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
|
|
193
215
|
inputSchema,
|
|
194
216
|
category: 'analysis',
|
|
195
217
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -256,14 +278,18 @@ export const GrepTool = defineTool({
|
|
|
256
278
|
'This sandbox does not support bounded file discovery (Sandbox.walkFiles). Update its adapter before searching file contents.',
|
|
257
279
|
}
|
|
258
280
|
}
|
|
281
|
+
const budget = openSearchBudget(context.abortSignal)
|
|
282
|
+
const signal = budget.signal
|
|
283
|
+
let walkEnd: SandboxWalkSummary | undefined
|
|
259
284
|
const options: SandboxWalkFilesOptions = {
|
|
260
285
|
pattern: filePattern,
|
|
261
|
-
signal
|
|
262
|
-
maxEntries:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
signal,
|
|
287
|
+
maxEntries: MAX_FILES_SEARCHED + 1,
|
|
288
|
+
includeHidden: input.include_hidden ?? true,
|
|
289
|
+
respectIgnore: input.include_ignored !== true,
|
|
290
|
+
onSummary: (value) => {
|
|
291
|
+
walkEnd = value
|
|
292
|
+
},
|
|
267
293
|
}
|
|
268
294
|
const entries = walkSandbox ? walkSandbox(root, options) : walkFilesLocally(root, options)
|
|
269
295
|
|
|
@@ -282,24 +308,33 @@ export const GrepTool = defineTool({
|
|
|
282
308
|
let filesSearched = 0
|
|
283
309
|
let filesMatched = 0
|
|
284
310
|
|
|
285
|
-
let truncated = false
|
|
286
311
|
let pageHasMore = false
|
|
287
312
|
let failure: string | undefined
|
|
288
313
|
let unreadableFiles = 0
|
|
314
|
+
let timedOut = false
|
|
315
|
+
let legacyEntryLimit = false
|
|
316
|
+
let fileLimit = false
|
|
317
|
+
// A file list is ordered newest first, so the walk cannot stop at the
|
|
318
|
+
// first page: it reads a generous multiple of it, then sorts and pages.
|
|
319
|
+
// The count includes the offset, so every page the model is told to ask
|
|
320
|
+
// for can be served.
|
|
321
|
+
const filesToCollect =
|
|
322
|
+
headLimit > 0 ? offset + Math.max(headLimit * 4, 1000) : Number.POSITIVE_INFINITY
|
|
323
|
+
let enough = false
|
|
289
324
|
try {
|
|
290
325
|
for await (const entry of entries) {
|
|
291
|
-
|
|
326
|
+
signal?.throwIfAborted()
|
|
292
327
|
const filePath = sandbox
|
|
293
328
|
? resolveWithinPosix(root, entry.path)
|
|
294
329
|
: resolveWithin(root, entry.path)
|
|
295
|
-
if (filesSearched >=
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
330
|
+
if (filesSearched >= MAX_FILES_SEARCHED) {
|
|
331
|
+
fileLimit = true
|
|
332
|
+
break
|
|
299
333
|
}
|
|
300
334
|
filesSearched++
|
|
301
335
|
if (fileType && !matchesType(entry.path, fileType)) continue
|
|
302
336
|
if (entry.size > MAX_FILE_SIZE) continue
|
|
337
|
+
if (isReadDenied(context, filePath, sandbox?.rootDir)) continue
|
|
303
338
|
|
|
304
339
|
let content: string
|
|
305
340
|
let contained: string | undefined
|
|
@@ -309,21 +344,21 @@ export const GrepTool = defineTool({
|
|
|
309
344
|
sandbox
|
|
310
345
|
? readBoundedChunks(
|
|
311
346
|
(offset, length) =>
|
|
312
|
-
sandbox.readFile(filePath, { offset, length, signal:
|
|
347
|
+
sandbox.readFile(filePath, { offset, length, signal: signal }),
|
|
313
348
|
MAX_FILE_SIZE,
|
|
314
|
-
|
|
349
|
+
signal,
|
|
315
350
|
)
|
|
316
351
|
: resolveWithinReal(root, filePath).then((path) => {
|
|
317
352
|
contained = path
|
|
318
|
-
return readBoundedFile(path, MAX_FILE_SIZE,
|
|
353
|
+
return readBoundedFile(path, MAX_FILE_SIZE, signal)
|
|
319
354
|
}),
|
|
320
|
-
|
|
355
|
+
signal,
|
|
321
356
|
)
|
|
322
|
-
|
|
357
|
+
signal?.throwIfAborted()
|
|
323
358
|
if (buffer.length > MAX_FILE_SIZE || isBinaryContent(buffer)) continue
|
|
324
359
|
content = buffer.toString('utf-8')
|
|
325
360
|
} catch (error) {
|
|
326
|
-
if (
|
|
361
|
+
if (signal?.aborted) throw error
|
|
327
362
|
unreadableFiles++
|
|
328
363
|
continue
|
|
329
364
|
}
|
|
@@ -331,14 +366,12 @@ export const GrepTool = defineTool({
|
|
|
331
366
|
const lines = content.split('\n')
|
|
332
367
|
let fileMatches = 0
|
|
333
368
|
const relPath = sandbox
|
|
334
|
-
?
|
|
335
|
-
:
|
|
336
|
-
.split(sep)
|
|
337
|
-
.join('/')}`
|
|
369
|
+
? relativePosix(sandbox.rootDir, filePath)
|
|
370
|
+
: hostDisplayPath(context.workingDirectory, join(lexicalRoot, relative(root, filePath)))
|
|
338
371
|
|
|
339
372
|
if (mode === 'content' && input.only_matching) {
|
|
340
373
|
for (const fragment of matchingFragments(content, regex, multiline)) {
|
|
341
|
-
|
|
374
|
+
signal?.throwIfAborted()
|
|
342
375
|
if (fileMatches++ === 0) filesMatched++
|
|
343
376
|
totalMatches++
|
|
344
377
|
results.push(`${relPath}:${fragment.line}:${clipLine(fragment.text, 'matching')}`)
|
|
@@ -356,7 +389,7 @@ export const GrepTool = defineTool({
|
|
|
356
389
|
// Count mode reports matching lines, so a line covered twice counts once.
|
|
357
390
|
const countedLines = new Set<number>()
|
|
358
391
|
for (const [first, last] of hits) {
|
|
359
|
-
|
|
392
|
+
signal?.throwIfAborted()
|
|
360
393
|
if (fileMatches === 0) filesMatched++
|
|
361
394
|
fileMatches++
|
|
362
395
|
totalMatches++
|
|
@@ -397,16 +430,32 @@ export const GrepTool = defineTool({
|
|
|
397
430
|
})
|
|
398
431
|
}
|
|
399
432
|
if (pageHasMore) break
|
|
433
|
+
if (mode === 'files_with_matches' && matchedFiles.length >= filesToCollect) {
|
|
434
|
+
enough = true
|
|
435
|
+
break
|
|
436
|
+
}
|
|
400
437
|
}
|
|
401
438
|
} catch (error) {
|
|
402
|
-
|
|
403
|
-
|
|
439
|
+
// A caller's cancellation is reported with the matches already found:
|
|
440
|
+
// grep-search-scope.test.ts pins that, so a cancelled search keeps what it read.
|
|
441
|
+
if (budget.expired()) timedOut = true
|
|
442
|
+
else if (isEntryLimit(error)) legacyEntryLimit = true
|
|
443
|
+
else
|
|
444
|
+
failure = (error instanceof Error ? error.message : String(error)) || 'File search failed.'
|
|
445
|
+
} finally {
|
|
446
|
+
budget.close()
|
|
404
447
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
448
|
+
const endState = walkEnd as SandboxWalkSummary | undefined
|
|
449
|
+
const entryLimit = endState?.stopped === 'entry-limit' || legacyEntryLimit
|
|
450
|
+
const stoppedEarly = timedOut || entryLimit || fileLimit
|
|
451
|
+
if (timedOut && totalMatches === 0 && failure === undefined) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
output: '',
|
|
455
|
+
error: `Search timed out after ${SEARCH_BUDGET_MS / 1000} seconds with no matches. Try a more specific path or pattern.`,
|
|
456
|
+
}
|
|
408
457
|
}
|
|
409
|
-
|
|
458
|
+
const truncated = stoppedEarly || pageHasMore || enough || failure !== undefined
|
|
410
459
|
|
|
411
460
|
// Entries as the model will page through them.
|
|
412
461
|
let entriesOut: string[]
|
|
@@ -422,7 +471,7 @@ export const GrepTool = defineTool({
|
|
|
422
471
|
headLimit === 0 ? entriesOut.slice(offset) : entriesOut.slice(offset, offset + headLimit)
|
|
423
472
|
// Reported only when it did something, like a `| head`: a limit that cut
|
|
424
473
|
// nothing off is not news.
|
|
425
|
-
const moreRemain = headLimit > 0 && (pageHasMore || total - offset > headLimit)
|
|
474
|
+
const moreRemain = headLimit > 0 && (pageHasMore || enough || total - offset > headLimit)
|
|
426
475
|
const paging = [
|
|
427
476
|
...(moreRemain ? [`limit: ${headLimit}`] : []),
|
|
428
477
|
...(offset > 0 ? [`offset: ${offset}`] : []),
|
|
@@ -435,9 +484,12 @@ export const GrepTool = defineTool({
|
|
|
435
484
|
summary =
|
|
436
485
|
offset > 0 && total > 0
|
|
437
486
|
? 'No entries at this offset.'
|
|
438
|
-
:
|
|
487
|
+
: stoppedEarly || failure !== undefined || unreadableFiles > 0
|
|
439
488
|
? `No matches found in the files searched for pattern "${input.pattern}"`
|
|
440
489
|
: `No matches found for pattern "${input.pattern}"`
|
|
490
|
+
if ((endState?.ignoredDirectories ?? 0) > 0 && offset === 0) {
|
|
491
|
+
summary = `${summary}\n${ignoredHint(endState?.ignoredDirectories ?? 0)}`
|
|
492
|
+
}
|
|
441
493
|
} else if (mode === 'files_with_matches') {
|
|
442
494
|
body = `Found ${page.length} ${page.length === 1 ? 'file' : 'files'}${paging ? ` (${paging})` : ''}\n${page.join('\n')}`
|
|
443
495
|
} else if (mode === 'count') {
|
|
@@ -447,21 +499,31 @@ export const GrepTool = defineTool({
|
|
|
447
499
|
body = page.join('\n')
|
|
448
500
|
summary = `Found ${totalMatches} match(es) in ${filesMatched} file(s) (${filesSearched} files searched)${offset > 0 ? ` from offset ${offset}` : ''}`
|
|
449
501
|
}
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
502
|
+
const notices = [
|
|
503
|
+
...(moreRemain
|
|
504
|
+
? [
|
|
505
|
+
`[More results are available: pass offset: ${offset + headLimit} to continue, or narrow the path or pattern.${enough ? ` Files are ordered newest first among the first ${filesToCollect.toLocaleString('en-US')} that matched.` : ''}]`,
|
|
506
|
+
]
|
|
507
|
+
: []),
|
|
508
|
+
...(stoppedEarly
|
|
509
|
+
? [stoppedNotice(timedOut ? 'time' : entryLimit ? 'entries' : 'files', endState?.visited)]
|
|
510
|
+
: []),
|
|
511
|
+
...(unreadableFiles > 0
|
|
512
|
+
? [
|
|
513
|
+
`(${plural(unreadableFiles, 'file')} could not be read and ${unreadableFiles === 1 ? 'was' : 'were'} skipped.)`,
|
|
514
|
+
]
|
|
515
|
+
: []),
|
|
516
|
+
...(failure ? [`[Search incomplete: ${failure}]`] : []),
|
|
517
|
+
]
|
|
457
518
|
return {
|
|
458
519
|
success: failure === undefined,
|
|
459
|
-
output: [body, summary,
|
|
520
|
+
output: [body, summary, ...notices].filter(Boolean).join('\n\n'),
|
|
460
521
|
data: {
|
|
461
522
|
totalMatches,
|
|
462
523
|
filesSearched,
|
|
463
524
|
filesMatched,
|
|
464
525
|
truncated,
|
|
526
|
+
...(stoppedEarly ? { incomplete: true } : {}),
|
|
465
527
|
mode,
|
|
466
528
|
...(moreRemain ? { appliedLimit: headLimit } : {}),
|
|
467
529
|
...(offset > 0 ? { appliedOffset: offset } : {}),
|
|
@@ -474,6 +536,8 @@ export const GrepTool = defineTool({
|
|
|
474
536
|
filesMatched,
|
|
475
537
|
filesSearched,
|
|
476
538
|
truncated,
|
|
539
|
+
...(moreRemain ? { limited: true } : {}),
|
|
540
|
+
...(stoppedEarly ? { incomplete: true } : {}),
|
|
477
541
|
},
|
|
478
542
|
...(failure ? { error: failure } : {}),
|
|
479
543
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
import { invalidBackgroundJobIdInput } from '../../runtime/jobs/job-id.js'
|
|
4
|
+
import {
|
|
5
|
+
formatLivenessAge,
|
|
6
|
+
formatLivenessLine,
|
|
7
|
+
judgeLiveness,
|
|
8
|
+
} from '../../runtime/jobs/liveness.js'
|
|
4
9
|
import { defineTool } from '../defineTool.js'
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -21,7 +26,7 @@ const inputSchema = z.object({
|
|
|
21
26
|
action: z
|
|
22
27
|
.enum(['read', 'list', 'kill'])
|
|
23
28
|
.describe(
|
|
24
|
-
'read: output since `from_offset`. list: every background job this turn started, with
|
|
29
|
+
'read: output since `from_offset`. list: every background job this turn started, each running one with whether it is progressing, waiting or stalled, how long since its last output, and its last output line. kill: stop a job and everything it forked.',
|
|
25
30
|
),
|
|
26
31
|
id: z
|
|
27
32
|
.string()
|
|
@@ -76,18 +81,33 @@ export const JobTool = defineTool({
|
|
|
76
81
|
if (input.action === 'list') {
|
|
77
82
|
const all = jobs.list()
|
|
78
83
|
if (all.length === 0) return { success: true, output: 'No background jobs.' }
|
|
84
|
+
const now = Date.now()
|
|
85
|
+
const lines = all.map((job) => {
|
|
86
|
+
const label = job.kind === 'monitor' ? (job.description ?? job.command) : job.command
|
|
87
|
+
if (job.startedAt === undefined) {
|
|
88
|
+
return `${job.id} ${job.status.padEnd(8)} ${job.kind === 'monitor' ? `monitor · ${label}` : label}`
|
|
89
|
+
}
|
|
90
|
+
const liveness = judgeLiveness(
|
|
91
|
+
{
|
|
92
|
+
kind: job.kind === 'monitor' ? 'monitor' : 'job',
|
|
93
|
+
id: job.id,
|
|
94
|
+
label,
|
|
95
|
+
...(job.lastOutputLine !== undefined ? { where: job.lastOutputLine } : {}),
|
|
96
|
+
startedAt: job.startedAt,
|
|
97
|
+
...(job.lastOutputAt !== undefined ? { lastProgressAt: job.lastOutputAt } : {}),
|
|
98
|
+
alive: job.status === 'running' && (jobs.isAlive ? jobs.isAlive(job.id) : true),
|
|
99
|
+
finished: job.status !== 'running',
|
|
100
|
+
},
|
|
101
|
+
now,
|
|
102
|
+
)
|
|
103
|
+
// An ended job keeps its status word, which carries the exit.
|
|
104
|
+
return job.status === 'running'
|
|
105
|
+
? formatLivenessLine(liveness)
|
|
106
|
+
: `${job.id} ${job.status.padEnd(8)} ${label}`
|
|
107
|
+
})
|
|
79
108
|
return {
|
|
80
109
|
success: true,
|
|
81
|
-
|
|
82
|
-
// and the description is what the model wrote to tell its watches apart.
|
|
83
|
-
output: all
|
|
84
|
-
.map(
|
|
85
|
-
(job) =>
|
|
86
|
-
`${job.id} ${job.status.padEnd(8)} ${
|
|
87
|
-
job.kind === 'monitor' ? `monitor · ${job.description ?? job.command}` : job.command
|
|
88
|
-
}`,
|
|
89
|
-
)
|
|
90
|
-
.join('\n'),
|
|
110
|
+
output: lines.join('\n'),
|
|
91
111
|
data: { jobs: all },
|
|
92
112
|
}
|
|
93
113
|
}
|
|
@@ -116,6 +136,11 @@ export const JobTool = defineTool({
|
|
|
116
136
|
const read = jobs.read(input.id, {
|
|
117
137
|
...(input.from_offset === undefined ? {} : { fromOffset: input.from_offset }),
|
|
118
138
|
})
|
|
139
|
+
// A stopped job read to its end is collected: its exit notice would only
|
|
140
|
+
// repeat what this result says. A read always reaches the end of the
|
|
141
|
+
// output; what it can miss is bytes the retention cap dropped, and then
|
|
142
|
+
// the notice stays.
|
|
143
|
+
if (read.status !== 'running' && read.droppedBytes === 0) jobs.markCollected?.(input.id)
|
|
119
144
|
// The dropped count is stated, never absorbed. A job whose middle
|
|
120
145
|
// vanished quietly reads as a complete result that happens to be
|
|
121
146
|
// short, and the model concludes the build passed.
|
|
@@ -123,9 +148,14 @@ export const JobTool = defineTool({
|
|
|
123
148
|
read.droppedBytes > 0
|
|
124
149
|
? `[${read.droppedBytes} bytes were dropped before this point — the job produced output faster than the retention cap holds]\n`
|
|
125
150
|
: ''
|
|
151
|
+
const lastOutputAt = read.status === 'running' ? jobs.get(input.id)?.lastOutputAt : undefined
|
|
126
152
|
const status =
|
|
127
153
|
read.status === 'running'
|
|
128
|
-
?
|
|
154
|
+
? `still running${
|
|
155
|
+
lastOutputAt === undefined
|
|
156
|
+
? '; no output yet'
|
|
157
|
+
: `; last output ${formatLivenessAge((Date.now() - lastOutputAt) / 1000)} ago`
|
|
158
|
+
}`
|
|
129
159
|
: read.error !== undefined
|
|
130
160
|
? `failed to start: ${read.error}`
|
|
131
161
|
: `${read.status}${read.exitCode === undefined ? '' : ` with code ${read.exitCode}`}`
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isAbsolute, join as joinPath, posix, relative, sep } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { subscribeToAbort } from '../../utils/abort.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How long grep and glob search before they return what they have. The tool
|
|
7
|
+
* deadline (15 s) cancels a call outright and discards its output; a search
|
|
8
|
+
* that ends itself a little earlier can still hand back its matches.
|
|
9
|
+
*/
|
|
10
|
+
export const SEARCH_BUDGET_MS = 12_000
|
|
11
|
+
|
|
12
|
+
/** Matching files a search reads at most; past it the search ends with what it has. */
|
|
13
|
+
export const MAX_FILES_SEARCHED = 50_000
|
|
14
|
+
|
|
15
|
+
export interface SearchBudget {
|
|
16
|
+
/** Fires when the caller cancels or the budget runs out. */
|
|
17
|
+
readonly signal: AbortSignal
|
|
18
|
+
/** True only when the budget ran out, not when the caller cancelled. */
|
|
19
|
+
expired(): boolean
|
|
20
|
+
close(): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A signal that follows `parent` and also fires when the search budget is spent. */
|
|
24
|
+
export function openSearchBudget(parent?: AbortSignal, budgetMs = SEARCH_BUDGET_MS): SearchBudget {
|
|
25
|
+
const controller = new AbortController()
|
|
26
|
+
let spent = false
|
|
27
|
+
const stopWatching = parent
|
|
28
|
+
? subscribeToAbort(parent, () => controller.abort(parent.reason))
|
|
29
|
+
: undefined
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
spent = true
|
|
32
|
+
controller.abort(new Error('search budget spent'))
|
|
33
|
+
}, budgetMs)
|
|
34
|
+
return {
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
expired: () => spent && !parent?.aborted,
|
|
37
|
+
close() {
|
|
38
|
+
clearTimeout(timer)
|
|
39
|
+
stopWatching?.()
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A file as the model sees it: relative to the working directory when it lies
|
|
46
|
+
* under it, otherwise absolute. Never prefixed with `./`.
|
|
47
|
+
*/
|
|
48
|
+
export function hostDisplayPath(workingDirectory: string, absolute: string): string {
|
|
49
|
+
const rel = relative(workingDirectory, absolute)
|
|
50
|
+
if (rel === '') return '.'
|
|
51
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return absolute
|
|
52
|
+
return sep === '/' ? rel : rel.split(sep).join('/')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Keep unusual filenames unambiguous without changing the returned path data. */
|
|
56
|
+
export function quoteControlPath(path: string): string {
|
|
57
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: quote filenames containing control bytes instead of creating fake result lines.
|
|
58
|
+
return /[\x00-\x1f\x7f]/.test(path) ? JSON.stringify(path) : path
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function plural(count: number, one: string, many = `${one}s`): string {
|
|
62
|
+
return `${count.toLocaleString('en-US')} ${count === 1 ? one : many}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The line that tells the model why a search stopped before it finished. */
|
|
66
|
+
export function stoppedNotice(reason: 'time' | 'entries' | 'files', visited?: number): string {
|
|
67
|
+
const how =
|
|
68
|
+
reason === 'time'
|
|
69
|
+
? `after ${SEARCH_BUDGET_MS / 1000} seconds`
|
|
70
|
+
: reason === 'entries'
|
|
71
|
+
? `after examining ${(visited ?? 0).toLocaleString('en-US')} entries`
|
|
72
|
+
: `after reading ${MAX_FILES_SEARCHED.toLocaleString('en-US')} files`
|
|
73
|
+
return `[Search stopped early ${how}. Results may be incomplete; narrow the path or pattern, or add an ignore rule.]`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Tells the model that an empty result may hide files an ignore rule excluded. */
|
|
77
|
+
export function ignoredHint(ignoredDirectories: number): string {
|
|
78
|
+
return `(${plural(ignoredDirectories, 'ignored directory', 'ignored directories')} skipped by ignore rules; pass include_ignored: true to search ${ignoredDirectories === 1 ? 'it' : 'them'}.)`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A walker written before the entry bound became a normal stop still throws this. */
|
|
82
|
+
export function isEntryLimit(error: unknown): boolean {
|
|
83
|
+
return (error as { code?: unknown } | null)?.code === 'ERR_FILE_WALK_LIMIT'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether the session's read rules deny this file, so a search neither lists
|
|
88
|
+
* nor opens it. `path` is absolute: a host path, or, when `sandboxRoot` is
|
|
89
|
+
* given, a path inside the sandbox, which is mapped to the same place under
|
|
90
|
+
* the working directory because that is the folder the sandbox binds; a path
|
|
91
|
+
* outside that folder is denied. A host
|
|
92
|
+
* that supplies no rules (`ToolContext.readDenied` absent) denies nothing.
|
|
93
|
+
*/
|
|
94
|
+
export function isReadDenied(
|
|
95
|
+
context: { workingDirectory: string; readDenied?: (path: string) => boolean },
|
|
96
|
+
path: string,
|
|
97
|
+
sandboxRoot?: string,
|
|
98
|
+
): boolean {
|
|
99
|
+
if (!context.readDenied) return false
|
|
100
|
+
if (sandboxRoot === undefined) return context.readDenied(path)
|
|
101
|
+
const relative = posix.relative(sandboxRoot, path)
|
|
102
|
+
// A guest path outside the bound folder has no host counterpart the rules
|
|
103
|
+
// could be checked against, so a session that has read rules does not list
|
|
104
|
+
// or open it.
|
|
105
|
+
if (relative === '..' || relative.startsWith('../') || posix.isAbsolute(relative)) return true
|
|
106
|
+
return context.readDenied(joinPath(context.workingDirectory, relative))
|
|
107
|
+
}
|