@namzu/sdk 51.1.0 → 52.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 +100 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +80 -4
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +36 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -0
- package/dist/authorization/protected-paths.js +107 -0
- package/dist/authorization/protected-paths.js.map +1 -0
- package/dist/authorization/readonly-commands.d.ts +39 -0
- package/dist/authorization/readonly-commands.d.ts.map +1 -0
- package/dist/authorization/readonly-commands.js +797 -0
- package/dist/authorization/readonly-commands.js.map +1 -0
- package/dist/authorization/readonly-flags.d.ts +19 -0
- package/dist/authorization/readonly-flags.d.ts.map +1 -0
- package/dist/authorization/readonly-flags.js +178 -0
- package/dist/authorization/readonly-flags.js.map +1 -0
- package/dist/authorization/removal.d.ts +52 -0
- package/dist/authorization/removal.d.ts.map +1 -0
- package/dist/authorization/removal.js +283 -0
- package/dist/authorization/removal.js.map +1 -0
- package/dist/authorization/shell-paths.d.ts +66 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -0
- package/dist/authorization/shell-paths.js +697 -0
- package/dist/authorization/shell-paths.js.map +1 -0
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/compaction/context-window.d.ts +24 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +34 -0
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +10 -2
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/plan.d.ts +4 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +6 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/verifier.d.ts +11 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +19 -7
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +55 -7
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +23 -2
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
- package/dist/connector/web/guarded-fetch.js +24 -0
- package/dist/connector/web/guarded-fetch.js.map +1 -1
- package/dist/connector/web/index.d.ts +1 -1
- package/dist/connector/web/index.d.ts.map +1 -1
- package/dist/connector/web/page.d.ts +59 -0
- package/dist/connector/web/page.d.ts.map +1 -0
- package/dist/connector/web/page.js +112 -0
- package/dist/connector/web/page.js.map +1 -0
- package/dist/connector/web/types.d.ts +47 -0
- package/dist/connector/web/types.d.ts.map +1 -1
- package/dist/connector/web/types.js.map +1 -1
- package/dist/constants/continuation.d.ts +8 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +8 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +1 -1
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +1 -1
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +7 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +11 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +4 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +6 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +98 -114
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +90 -22
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +434 -41
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- 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 +15 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +74 -13
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +41 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +190 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +6 -0
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +1 -1
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +15 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +31 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +216 -7
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +58 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +91 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +89 -7
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -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 +165 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +36 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -0
- package/dist/runtime/query/path-escalation.js +184 -0
- package/dist/runtime/query/path-escalation.js.map +1 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +8 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/reminders.d.ts +35 -0
- package/dist/runtime/query/reminders.d.ts.map +1 -0
- package/dist/runtime/query/reminders.js +7 -0
- package/dist/runtime/query/reminders.js.map +1 -0
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +26 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +26 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +79 -5
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +46 -0
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +85 -1
- package/dist/runtime/query/tool-output-budget.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 +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/session/paths.d.ts +5 -1
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +8 -0
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +2 -2
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +2 -2
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/naming.d.ts +9 -0
- package/dist/store/memory/naming.d.ts.map +1 -1
- package/dist/store/memory/naming.js +12 -0
- package/dist/store/memory/naming.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +30 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +170 -26
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +4 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +60 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +152 -33
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +44 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +52 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +143 -0
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/web.d.ts +13 -2
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +225 -32
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +256 -92
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/question-options.d.ts +1 -0
- package/dist/tools/coordinator/question-options.d.ts.map +1 -1
- package/dist/tools/coordinator/question-options.js +6 -1
- package/dist/tools/coordinator/question-options.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -1
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +6 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/paths.d.ts +6 -0
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +1 -1
- package/dist/tools/paths.js.map +1 -1
- package/dist/types/agent/query.d.ts +3 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +104 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +93 -19
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +3 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/errors.d.ts +19 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +24 -0
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/session/events.d.ts +10 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +9 -9
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +5 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +52 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +4 -2
- package/src/agents/QueryAgent.ts +5 -1
- package/src/agents/SupervisorAgent.ts +1 -1
- package/src/agents/examples/SupervisorAgent.ts +217 -106
- package/src/agents/index.ts +1 -1
- package/src/authorization/protected-paths.ts +110 -0
- package/src/authorization/readonly-commands.ts +771 -0
- package/src/authorization/readonly-flags.ts +196 -0
- package/src/authorization/removal.ts +326 -0
- package/src/authorization/shell-paths.ts +927 -0
- package/src/bridge/acp/update.ts +1 -0
- package/src/compaction/context-window.ts +41 -0
- package/src/compaction/manual.ts +10 -2
- package/src/compaction/plan.ts +11 -2
- package/src/compaction/verifier.ts +28 -3
- package/src/config/runtime.ts +23 -2
- package/src/connector/web/guarded-fetch.ts +26 -0
- package/src/connector/web/index.ts +3 -0
- package/src/connector/web/page.ts +130 -0
- package/src/connector/web/types.ts +47 -0
- package/src/constants/continuation.ts +10 -0
- package/src/constants/plugin/index.ts +1 -1
- package/src/manager/agent/lifecycle.ts +12 -4
- package/src/manager/session/turn-recorder.ts +10 -2
- package/src/plugin/index.ts +1 -0
- package/src/plugin/lifecycle.ts +107 -119
- package/src/plugin/shell-hook.ts +477 -43
- package/src/public-runtime.ts +8 -0
- package/src/public-types.ts +4 -0
- package/src/runtime/jobs/registry.ts +39 -2
- package/src/runtime/query/executor/tool-call-admission.ts +79 -13
- package/src/runtime/query/executor.ts +242 -8
- package/src/runtime/query/finalize-turn.ts +7 -1
- package/src/runtime/query/index.ts +45 -3
- package/src/runtime/query/iteration/index.ts +251 -12
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +106 -0
- package/src/runtime/query/iteration/phases/compaction.ts +99 -12
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
- package/src/runtime/query/path-escalation.ts +204 -0
- package/src/runtime/query/plugin-hooks.ts +8 -0
- package/src/runtime/query/reminders.ts +45 -0
- package/src/runtime/query/resume-pending.ts +27 -0
- package/src/runtime/query/review-policy.ts +106 -5
- package/src/runtime/query/tool-output-budget.ts +115 -1
- package/src/runtime/query/tooling.ts +6 -0
- package/src/session/paths.ts +11 -1
- package/src/store/memory/disk.ts +2 -1
- package/src/store/memory/markdown.ts +2 -1
- package/src/store/memory/naming.ts +13 -0
- package/src/tools/builtins/bash.ts +203 -34
- package/src/tools/builtins/content-fingerprint.ts +5 -0
- package/src/tools/builtins/edit.ts +69 -19
- package/src/tools/builtins/grep.ts +159 -38
- package/src/tools/builtins/read-file.ts +48 -5
- package/src/tools/builtins/read-render.ts +193 -0
- package/src/tools/builtins/web.ts +258 -33
- package/src/tools/coordinator/ask-user-question.ts +309 -103
- package/src/tools/coordinator/question-options.ts +8 -1
- package/src/tools/defineTool.ts +11 -2
- package/src/tools/paths.ts +1 -1
- package/src/types/agent/query.ts +3 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +104 -0
- package/src/types/message/index.ts +2 -0
- package/src/types/plugin/index.ts +101 -18
- package/src/types/provider/errors.ts +33 -0
- package/src/types/session/events.ts +10 -2
- package/src/types/session/records.ts +1 -0
- package/src/types/session/stop-reason.ts +5 -0
- package/src/types/tool/index.ts +53 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { readFile, 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'
|
|
@@ -51,6 +51,12 @@ function relativeInclude(include: string, root: string, sandboxed: boolean): str
|
|
|
51
51
|
return pattern.includes('/') ? pattern : `**/${pattern}`
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Entries returned when `head_limit` is not given. */
|
|
55
|
+
export const DEFAULT_HEAD_LIMIT = 250
|
|
56
|
+
|
|
57
|
+
/** A matching or context line longer than this is replaced by a notice, not printed. */
|
|
58
|
+
export const MAX_LINE_COLUMNS = 500
|
|
59
|
+
|
|
54
60
|
const inputSchema = z.object({
|
|
55
61
|
pattern: z.string().describe('Regular expression pattern to search for in file contents'),
|
|
56
62
|
path: z
|
|
@@ -68,12 +74,32 @@ const inputSchema = z.object({
|
|
|
68
74
|
.min(0)
|
|
69
75
|
.default(0)
|
|
70
76
|
.describe('Number of lines to show before and after each match'),
|
|
77
|
+
output_mode: z
|
|
78
|
+
.enum(['files_with_matches', 'content', 'count'])
|
|
79
|
+
.default('files_with_matches')
|
|
80
|
+
.describe(
|
|
81
|
+
'What to return. "files_with_matches" (default): the paths of files that match, newest first. "content": matching lines with file and line number (supports context_lines). "count": the number of matching lines per file.',
|
|
82
|
+
),
|
|
83
|
+
head_limit: z.coerce
|
|
84
|
+
.number()
|
|
85
|
+
.int()
|
|
86
|
+
.min(0)
|
|
87
|
+
.optional()
|
|
88
|
+
.describe(
|
|
89
|
+
`Return at most this many entries (file paths, output lines, or count rows depending on output_mode). Defaults to ${DEFAULT_HEAD_LIMIT}. Pass 0 for no limit; use that sparingly, a large result wastes context.`,
|
|
90
|
+
),
|
|
91
|
+
offset: z.coerce
|
|
92
|
+
.number()
|
|
93
|
+
.int()
|
|
94
|
+
.min(0)
|
|
95
|
+
.default(0)
|
|
96
|
+
.describe('Skip this many entries before applying head_limit, to page through results.'),
|
|
71
97
|
max_results: z.coerce
|
|
72
98
|
.number()
|
|
73
99
|
.int()
|
|
74
100
|
.min(1)
|
|
75
|
-
.
|
|
76
|
-
.describe('
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Deprecated: use head_limit. Used as head_limit when head_limit is not given.'),
|
|
77
103
|
})
|
|
78
104
|
|
|
79
105
|
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB — skip binaries/large files
|
|
@@ -91,7 +117,7 @@ function isBinaryContent(buffer: Buffer): boolean {
|
|
|
91
117
|
export const GrepTool = defineTool({
|
|
92
118
|
name: 'grep',
|
|
93
119
|
description:
|
|
94
|
-
'Searches file contents using a regular expression.
|
|
120
|
+
'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. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
|
|
95
121
|
inputSchema,
|
|
96
122
|
category: 'analysis',
|
|
97
123
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -158,12 +184,21 @@ export const GrepTool = defineTool({
|
|
|
158
184
|
}
|
|
159
185
|
const entries = walkSandbox ? walkSandbox(root, options) : walkFilesLocally(root, options)
|
|
160
186
|
|
|
187
|
+
// `execute` is reachable without the schema's defaults.
|
|
188
|
+
const mode = input.output_mode ?? 'files_with_matches'
|
|
189
|
+
// `max_results` predates `head_limit`; it keeps meaning "at most this many".
|
|
190
|
+
const headLimit = input.head_limit ?? input.max_results ?? DEFAULT_HEAD_LIMIT
|
|
191
|
+
const offset = input.offset ?? 0
|
|
192
|
+
// Content lines in output order. Only content mode uses them as the
|
|
193
|
+
// result; the other modes build from `matchedFiles`.
|
|
161
194
|
const results: string[] = []
|
|
195
|
+
const matchedFiles: { relPath: string; host?: string; count: number }[] = []
|
|
162
196
|
let totalMatches = 0
|
|
163
197
|
let filesSearched = 0
|
|
164
198
|
let filesMatched = 0
|
|
165
199
|
|
|
166
200
|
let truncated = false
|
|
201
|
+
let pageHasMore = false
|
|
167
202
|
let failure: string | undefined
|
|
168
203
|
let unreadableFiles = 0
|
|
169
204
|
try {
|
|
@@ -181,14 +216,16 @@ export const GrepTool = defineTool({
|
|
|
181
216
|
if (entry.size > MAX_FILE_SIZE) continue
|
|
182
217
|
|
|
183
218
|
let content: string
|
|
219
|
+
let contained: string | undefined
|
|
184
220
|
try {
|
|
185
221
|
const buffer = await readWithSignal(
|
|
186
222
|
() =>
|
|
187
223
|
sandbox
|
|
188
224
|
? sandbox.readFile(filePath)
|
|
189
|
-
: resolveWithinReal(root, filePath).then((
|
|
190
|
-
|
|
191
|
-
|
|
225
|
+
: resolveWithinReal(root, filePath).then((path) => {
|
|
226
|
+
contained = path
|
|
227
|
+
return readFile(path, { signal: context.abortSignal })
|
|
228
|
+
}),
|
|
192
229
|
context.abortSignal,
|
|
193
230
|
)
|
|
194
231
|
context.abortSignal?.throwIfAborted()
|
|
@@ -201,7 +238,12 @@ export const GrepTool = defineTool({
|
|
|
201
238
|
}
|
|
202
239
|
|
|
203
240
|
const lines = content.split('\n')
|
|
204
|
-
let
|
|
241
|
+
let fileMatches = 0
|
|
242
|
+
const relPath = sandbox
|
|
243
|
+
? `./${relativePosix(sandbox.rootDir, filePath)}`
|
|
244
|
+
: `./${relative(context.workingDirectory, join(lexicalRoot, relative(root, filePath)))
|
|
245
|
+
.split(sep)
|
|
246
|
+
.join('/')}`
|
|
205
247
|
|
|
206
248
|
for (let i = 0; i < lines.length; i++) {
|
|
207
249
|
context.abortSignal?.throwIfAborted()
|
|
@@ -209,16 +251,12 @@ export const GrepTool = defineTool({
|
|
|
209
251
|
regex.lastIndex = 0
|
|
210
252
|
if (!regex.test(line)) continue
|
|
211
253
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
? `./${relativePosix(sandbox.rootDir, filePath)}`
|
|
219
|
-
: `./${relative(context.workingDirectory, join(lexicalRoot, relative(root, filePath)))
|
|
220
|
-
.split(sep)
|
|
221
|
-
.join('/')}`
|
|
254
|
+
if (fileMatches === 0) filesMatched++
|
|
255
|
+
fileMatches++
|
|
256
|
+
totalMatches++
|
|
257
|
+
// A file list only needs to know that the file matches.
|
|
258
|
+
if (mode === 'files_with_matches') break
|
|
259
|
+
if (mode === 'count') continue
|
|
222
260
|
|
|
223
261
|
if (input.context_lines > 0) {
|
|
224
262
|
const start = Math.max(0, i - input.context_lines)
|
|
@@ -230,22 +268,32 @@ export const GrepTool = defineTool({
|
|
|
230
268
|
|
|
231
269
|
for (let j = start; j <= end; j++) {
|
|
232
270
|
const prefix = j === i ? ':' : '-'
|
|
233
|
-
|
|
271
|
+
const text = lines[j] ?? ''
|
|
272
|
+
results.push(
|
|
273
|
+
`${relPath}${prefix}${j + 1}${prefix}${clipLine(text, j === i ? 'matching' : 'context')}`,
|
|
274
|
+
)
|
|
234
275
|
}
|
|
235
276
|
} else {
|
|
236
|
-
results.push(`${relPath}:${i + 1}:${line}`)
|
|
277
|
+
results.push(`${relPath}:${i + 1}:${clipLine(line, 'matching')}`)
|
|
237
278
|
}
|
|
238
279
|
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
// A full page ends the search: another file is not read just to
|
|
281
|
+
// prove there is more, so a page that is exactly full is reported
|
|
282
|
+
// as possibly continuing.
|
|
283
|
+
if (headLimit > 0 && results.length >= offset + headLimit) {
|
|
284
|
+
pageHasMore = true
|
|
285
|
+
break
|
|
286
|
+
}
|
|
241
287
|
}
|
|
242
288
|
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
289
|
+
if (fileMatches > 0 && mode !== 'content') {
|
|
290
|
+
matchedFiles.push({
|
|
291
|
+
relPath,
|
|
292
|
+
...(contained !== undefined ? { host: contained } : {}),
|
|
293
|
+
count: fileMatches,
|
|
294
|
+
})
|
|
248
295
|
}
|
|
296
|
+
if (pageHasMore) break
|
|
249
297
|
}
|
|
250
298
|
} catch (error) {
|
|
251
299
|
truncated = true
|
|
@@ -255,21 +303,66 @@ export const GrepTool = defineTool({
|
|
|
255
303
|
truncated = true
|
|
256
304
|
failure ??= `${unreadableFiles} file(s) could not be read.`
|
|
257
305
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
306
|
+
if (pageHasMore) truncated = true
|
|
307
|
+
|
|
308
|
+
// Entries as the model will page through them.
|
|
309
|
+
let entriesOut: string[]
|
|
310
|
+
if (mode === 'content') {
|
|
311
|
+
entriesOut = results
|
|
312
|
+
} else if (mode === 'count') {
|
|
313
|
+
entriesOut = matchedFiles.map((f) => `${f.relPath}:${f.count}`)
|
|
314
|
+
} else {
|
|
315
|
+
entriesOut = (await sortByNewest(matchedFiles)).map((f) => f.relPath)
|
|
316
|
+
}
|
|
317
|
+
const total = entriesOut.length
|
|
318
|
+
const page =
|
|
319
|
+
headLimit === 0 ? entriesOut.slice(offset) : entriesOut.slice(offset, offset + headLimit)
|
|
320
|
+
// Reported only when it did something, like a `| head`: a limit that cut
|
|
321
|
+
// nothing off is not news.
|
|
322
|
+
const moreRemain = headLimit > 0 && (pageHasMore || total - offset > headLimit)
|
|
323
|
+
const paging = [
|
|
324
|
+
...(moreRemain ? [`limit: ${headLimit}`] : []),
|
|
325
|
+
...(offset > 0 ? [`offset: ${offset}`] : []),
|
|
326
|
+
].join(', ')
|
|
327
|
+
|
|
328
|
+
let body: string
|
|
329
|
+
let summary: string | undefined
|
|
330
|
+
if (page.length === 0) {
|
|
331
|
+
body = ''
|
|
332
|
+
summary =
|
|
333
|
+
offset > 0 && total > 0
|
|
334
|
+
? 'No entries at this offset.'
|
|
335
|
+
: truncated
|
|
336
|
+
? `No matches found in the files searched for pattern "${input.pattern}"`
|
|
337
|
+
: `No matches found for pattern "${input.pattern}"`
|
|
338
|
+
} else if (mode === 'files_with_matches') {
|
|
339
|
+
body = `Found ${page.length} ${page.length === 1 ? 'file' : 'files'}${paging ? ` (${paging})` : ''}\n${page.join('\n')}`
|
|
340
|
+
} else if (mode === 'count') {
|
|
341
|
+
body = page.join('\n')
|
|
342
|
+
summary = `Found ${totalMatches} total ${totalMatches === 1 ? 'match' : 'matches'} across ${filesMatched} ${filesMatched === 1 ? 'file' : 'files'}${paging ? ` (${paging})` : ''}. ${filesSearched} files searched.`
|
|
343
|
+
} else {
|
|
344
|
+
body = page.join('\n')
|
|
345
|
+
summary = `Found ${totalMatches} match(es) in ${filesMatched} file(s) (${filesSearched} files searched)${offset > 0 ? ` from offset ${offset}` : ''}`
|
|
346
|
+
}
|
|
264
347
|
const notice = failure
|
|
265
348
|
? `[Search incomplete: ${failure}]`
|
|
266
|
-
:
|
|
267
|
-
? `[Search incomplete:
|
|
268
|
-
:
|
|
349
|
+
: pageHasMore
|
|
350
|
+
? `[Search incomplete: the page of ${headLimit} is full, so more may follow. Pass offset to continue, or narrow the directory or include pattern.]`
|
|
351
|
+
: truncated
|
|
352
|
+
? '[Search incomplete: the search did not reach every file. Narrow the directory or include pattern for a complete search.]'
|
|
353
|
+
: undefined
|
|
269
354
|
return {
|
|
270
355
|
success: failure === undefined,
|
|
271
|
-
output: [
|
|
272
|
-
data: {
|
|
356
|
+
output: [body, summary, notice].filter(Boolean).join('\n\n'),
|
|
357
|
+
data: {
|
|
358
|
+
totalMatches,
|
|
359
|
+
filesSearched,
|
|
360
|
+
filesMatched,
|
|
361
|
+
truncated,
|
|
362
|
+
mode,
|
|
363
|
+
...(moreRemain ? { appliedLimit: headLimit } : {}),
|
|
364
|
+
...(offset > 0 ? { appliedOffset: offset } : {}),
|
|
365
|
+
},
|
|
273
366
|
facts: {
|
|
274
367
|
kind: 'search',
|
|
275
368
|
pattern: input.pattern,
|
|
@@ -283,3 +376,31 @@ export const GrepTool = defineTool({
|
|
|
283
376
|
}
|
|
284
377
|
},
|
|
285
378
|
})
|
|
379
|
+
|
|
380
|
+
/** A line past the column cap is not printed: one minified bundle would be the whole result. */
|
|
381
|
+
function clipLine(line: string, kind: 'matching' | 'context'): string {
|
|
382
|
+
return line.length > MAX_LINE_COLUMNS ? `[Omitted long ${kind} line]` : line
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Newest modification first; ties by path. A file that cannot be stat'ed, and
|
|
387
|
+
* every file in a sandbox (which reports no modification time), sorts as
|
|
388
|
+
* oldest, so a sandbox listing is in path order.
|
|
389
|
+
*/
|
|
390
|
+
async function sortByNewest<T extends { relPath: string; host?: string }>(
|
|
391
|
+
files: readonly T[],
|
|
392
|
+
): Promise<T[]> {
|
|
393
|
+
const stamped = await Promise.all(
|
|
394
|
+
files.map(async (file) => {
|
|
395
|
+
if (file.host === undefined) return { file, mtime: 0 }
|
|
396
|
+
try {
|
|
397
|
+
return { file, mtime: (await stat(file.host)).mtimeMs }
|
|
398
|
+
} catch {
|
|
399
|
+
return { file, mtime: 0 }
|
|
400
|
+
}
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
return stamped
|
|
404
|
+
.sort((a, b) => b.mtime - a.mtime || a.file.relPath.localeCompare(b.file.relPath))
|
|
405
|
+
.map((entry) => entry.file)
|
|
406
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
2
2
|
import { extname } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import type { ToolContext } from '../../types/tool/index.js'
|
|
@@ -7,7 +7,7 @@ import { defineTool } from '../defineTool.js'
|
|
|
7
7
|
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
8
8
|
import { looksBinary } from '../text-window.js'
|
|
9
9
|
import { fingerprintContent } from './content-fingerprint.js'
|
|
10
|
-
import { type RenderedRead,
|
|
10
|
+
import { type RenderedRead, renderCappedRead } from './read-render.js'
|
|
11
11
|
|
|
12
12
|
const inputSchema = z.object({
|
|
13
13
|
path: z.string().describe('Path to the file to read (absolute or relative)'),
|
|
@@ -34,12 +34,18 @@ const inputSchema = z.object({
|
|
|
34
34
|
.optional()
|
|
35
35
|
.describe('Starting line number (0-indexed). Defaults to 0 (beginning of file).'),
|
|
36
36
|
limit: z.coerce.number().optional().describe('Maximum number of lines to read'),
|
|
37
|
+
allow_large: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe(
|
|
41
|
+
'Raise the size and token caps for this one read. Set it only after a read was refused as too large and you genuinely need all of the file.',
|
|
42
|
+
),
|
|
37
43
|
})
|
|
38
44
|
|
|
39
45
|
export const ReadFileTool = defineTool({
|
|
40
46
|
name: 'read',
|
|
41
47
|
description:
|
|
42
|
-
'Reads a file and returns its contents with line numbers. Supports readRange ([start,end], 1-indexed inclusive) or offset/limit for large files. Without a window it returns the first 2000 lines and says so — pass offset/limit to continue.',
|
|
48
|
+
'Reads a file and returns its contents with line numbers. Supports readRange ([start,end], 1-indexed inclusive) or offset/limit for large files. Without a window it returns the first 2000 lines and says so — pass offset/limit to continue. A file over 256 KiB is refused unless a window is named, and a result over about 25,000 tokens is cut to a first page that names the next call; pass allow_large only when you need all of it.',
|
|
43
49
|
inputSchema,
|
|
44
50
|
category: 'filesystem',
|
|
45
51
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -49,6 +55,9 @@ export const ReadFileTool = defineTool({
|
|
|
49
55
|
readOnly: true,
|
|
50
56
|
destructive: false,
|
|
51
57
|
concurrencySafe: true,
|
|
58
|
+
// Bounded by the size and token caps in `renderCappedRead`, so the turn's
|
|
59
|
+
// default per-result cap and the per-message budget leave it alone.
|
|
60
|
+
selfLimitingOutput: true,
|
|
52
61
|
|
|
53
62
|
presentCall(input) {
|
|
54
63
|
if (typeof input.path !== 'string') return undefined
|
|
@@ -84,7 +93,9 @@ export const ReadFileTool = defineTool({
|
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
const content = buffer.toString('utf-8')
|
|
87
|
-
const
|
|
96
|
+
const capped = renderCappedRead(content, buffer.length, input, capOptions(input, context))
|
|
97
|
+
if (!capped.ok) return refusedRead(input.path, buffer.length, capped.message, true)
|
|
98
|
+
const rendered = capped.rendered
|
|
88
99
|
recordObservedRead(context, input.path, content, rendered)
|
|
89
100
|
|
|
90
101
|
return {
|
|
@@ -102,6 +113,13 @@ export const ReadFileTool = defineTool({
|
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
const filePath = await resolveWithinAnyReal(toolRoots(context), input.path)
|
|
116
|
+
// Refused on the size alone, before the file is in memory, when no window
|
|
117
|
+
// was named: the cap exists so a huge file is never loaded to be refused.
|
|
118
|
+
if (input.readRange === undefined && input.limit === undefined) {
|
|
119
|
+
const { size } = await stat(filePath)
|
|
120
|
+
const early = renderCappedRead('', size, input, capOptions(input, context))
|
|
121
|
+
if (!early.ok) return refusedRead(filePath, size, early.message, false)
|
|
122
|
+
}
|
|
105
123
|
const buffer = await readFile(filePath)
|
|
106
124
|
const binaryGuidance = describeStructuredBinaryRead(filePath, buffer)
|
|
107
125
|
if (binaryGuidance) {
|
|
@@ -121,7 +139,9 @@ export const ReadFileTool = defineTool({
|
|
|
121
139
|
}
|
|
122
140
|
}
|
|
123
141
|
const content = buffer.toString('utf-8')
|
|
124
|
-
const
|
|
142
|
+
const capped = renderCappedRead(content, buffer.length, input, capOptions(input, context))
|
|
143
|
+
if (!capped.ok) return refusedRead(filePath, buffer.length, capped.message, false)
|
|
144
|
+
const rendered = capped.rendered
|
|
125
145
|
recordObservedRead(context, filePath, content, rendered)
|
|
126
146
|
|
|
127
147
|
return {
|
|
@@ -138,6 +158,29 @@ export const ReadFileTool = defineTool({
|
|
|
138
158
|
},
|
|
139
159
|
})
|
|
140
160
|
|
|
161
|
+
function capOptions(
|
|
162
|
+
input: { path: string; allow_large?: boolean | undefined },
|
|
163
|
+
context: ToolContext,
|
|
164
|
+
): Parameters<typeof renderCappedRead>[3] {
|
|
165
|
+
const tokens = Number(context.env?.NAMZU_FILE_READ_MAX_OUTPUT_TOKENS)
|
|
166
|
+
return {
|
|
167
|
+
path: input.path,
|
|
168
|
+
ext: extname(input.path).toLowerCase(),
|
|
169
|
+
...(input.allow_large === true ? { allowLarge: true } : {}),
|
|
170
|
+
...(Number.isFinite(tokens) && tokens > 0 ? { caps: { maxTokens: tokens } } : {}),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function refusedRead(path: string, bytes: number, message: string, sandboxed: boolean) {
|
|
175
|
+
return {
|
|
176
|
+
success: false as const,
|
|
177
|
+
output: '',
|
|
178
|
+
error: message,
|
|
179
|
+
data: { path, tooLarge: true, ...(sandboxed ? { sandboxed: true } : {}) },
|
|
180
|
+
facts: { kind: 'read' as const, path, bytes },
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
141
184
|
/**
|
|
142
185
|
* What a read returned, as fields. A file that is not text is named as such,
|
|
143
186
|
* so a host says "Binary file, 4 KB" instead of drawing the bytes the model's
|
|
@@ -102,3 +102,196 @@ export function resolveReadWindow(
|
|
|
102
102
|
const end = input.limit ? start + input.limit : Math.min(totalLines, start + DEFAULT_READ_LINES)
|
|
103
103
|
return { start, end }
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Largest file `read` returns whole when the caller named no window.
|
|
108
|
+
* A file over it is refused with the way to read part of it.
|
|
109
|
+
*/
|
|
110
|
+
export const MAX_READ_BYTES = 262_144
|
|
111
|
+
|
|
112
|
+
/** Largest read result, in estimated tokens. */
|
|
113
|
+
export const MAX_READ_TOKENS = 25_000
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Bytes of text one token is allowed to stand for when bounding a SELECTED
|
|
117
|
+
* window by its bytes: a window may hold at most `tokens * 128` bytes before
|
|
118
|
+
* it is refused outright, without estimating anything.
|
|
119
|
+
*/
|
|
120
|
+
const SELECTED_BYTES_PER_TOKEN = 128
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What `allow_large` multiplies both caps by.
|
|
124
|
+
*
|
|
125
|
+
* The caps it lifts are "what still fits in the context left this turn",
|
|
126
|
+
* which this kernel does not track, so it lifts them by a fixed factor.
|
|
127
|
+
*/
|
|
128
|
+
export const ALLOW_LARGE_FACTOR = 4
|
|
129
|
+
|
|
130
|
+
/** Share of the page a trimmed first page is aimed at, so the estimate lands under the cap. */
|
|
131
|
+
const TRIM_TARGET = 0.85
|
|
132
|
+
|
|
133
|
+
/** The ways a read can be refused before it returns anything. */
|
|
134
|
+
export type ReadRefusal = string
|
|
135
|
+
|
|
136
|
+
export interface ReadCaps {
|
|
137
|
+
readonly maxBytes: number
|
|
138
|
+
readonly maxTokens: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ReadCapOptions {
|
|
142
|
+
readonly path: string
|
|
143
|
+
/** File extension with the dot, lower case; JSON text is denser per token. */
|
|
144
|
+
readonly ext: string
|
|
145
|
+
readonly allowLarge?: boolean
|
|
146
|
+
/** Overrides for the defaults, from the host's environment. */
|
|
147
|
+
readonly caps?: Partial<ReadCaps>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Estimated tokens: characters over four, over two for JSON text. */
|
|
151
|
+
export function estimateReadTokens(text: string, ext: string): number {
|
|
152
|
+
const perToken = ext === '.json' || ext === '.jsonl' || ext === '.jsonc' ? 2 : 4
|
|
153
|
+
return Math.round(text.length / perToken)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function humanBytes(n: number): string {
|
|
157
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`
|
|
158
|
+
if (n >= 1024) return `${Math.round(n / 1024)} KiB`
|
|
159
|
+
return `${n} B`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function allowLargeHint(allowedAlready: boolean): string {
|
|
163
|
+
return allowedAlready
|
|
164
|
+
? ' Even with allow_large this is more than a read may return.'
|
|
165
|
+
: ' To read it anyway, call read again with allow_large: true, but only if you need all of it or were asked for the whole file.'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Render a read, or refuse it, under the size and token caps.
|
|
170
|
+
*
|
|
171
|
+
* Three rules, in the order they are checked:
|
|
172
|
+
*
|
|
173
|
+
* 1. No window named and the file over {@link MAX_READ_BYTES}: refuse. A caller
|
|
174
|
+
* that asked for a window (`readRange`, `limit`) has chosen its size.
|
|
175
|
+
* 2. A selected window over `maxTokens * 128` bytes: refuse.
|
|
176
|
+
* 3. The selected text over `maxTokens` by estimate: for the first page of an
|
|
177
|
+
* unwindowed read, return the lines that fit with a notice naming the next
|
|
178
|
+
* call; for any other read, refuse and report the count.
|
|
179
|
+
*
|
|
180
|
+
* Pure, like {@link renderNumberedRead}; a read that none of this touches
|
|
181
|
+
* renders byte for byte as `renderNumberedRead` does.
|
|
182
|
+
*/
|
|
183
|
+
export function renderCappedRead(
|
|
184
|
+
content: string,
|
|
185
|
+
byteLength: number,
|
|
186
|
+
input: ReadWindowRequest,
|
|
187
|
+
options: ReadCapOptions,
|
|
188
|
+
): { ok: true; rendered: RenderedRead } | { ok: false; message: ReadRefusal } {
|
|
189
|
+
const factor = options.allowLarge ? ALLOW_LARGE_FACTOR : 1
|
|
190
|
+
const maxBytes = (options.caps?.maxBytes ?? MAX_READ_BYTES) * factor
|
|
191
|
+
const maxTokens = (options.caps?.maxTokens ?? MAX_READ_TOKENS) * factor
|
|
192
|
+
const hint = allowLargeHint(options.allowLarge === true)
|
|
193
|
+
|
|
194
|
+
const windowed = input.readRange !== undefined || input.limit !== undefined
|
|
195
|
+
if (!windowed && byteLength > maxBytes) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
message: `File content (${humanBytes(byteLength)}) exceeds the maximum allowed size (${humanBytes(maxBytes)}). Use offset and limit to read part of it, or search for what you need instead of reading all of it.${hint}`,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const lines = content.split('\n')
|
|
203
|
+
const { start, end } = resolveReadWindow(input, lines.length)
|
|
204
|
+
const selected = lines.slice(start, end)
|
|
205
|
+
const selectedText = selected.join('\n')
|
|
206
|
+
|
|
207
|
+
const selectedBytes = Buffer.byteLength(selectedText, 'utf8')
|
|
208
|
+
if (selectedBytes > maxTokens * SELECTED_BYTES_PER_TOKEN) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
message: `The requested lines hold over ${humanBytes(maxTokens * SELECTED_BYTES_PER_TOKEN)} of text, more than a read can return. Use a smaller limit; if a single line is this large, no limit will fit it, so search for what you need instead.${hint}`,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tokens = estimateReadTokens(selectedText, options.ext)
|
|
216
|
+
if (tokens <= maxTokens) {
|
|
217
|
+
return { ok: true, rendered: renderNumberedRead(content, input) }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const firstPage = input.readRange === undefined && input.limit === undefined && start === 0
|
|
221
|
+
if (!firstPage) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
message: `File content (${tokens} tokens) exceeds the maximum allowed tokens (${maxTokens}). Use offset and limit to read part of it, or search for what you need instead of reading all of it.${hint}`,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
rendered: trimFirstPage(selected, lines.length, selectedText, tokens, maxTokens, options.path),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The first page of a file whose first window is over the token cap: the
|
|
235
|
+
* leading lines that fit, numbered, and a notice that says it is a page and
|
|
236
|
+
* names the next call.
|
|
237
|
+
*
|
|
238
|
+
* Sized from the measured ratio of characters to tokens, aimed at
|
|
239
|
+
* {@link TRIM_TARGET} of the cap, and shrunk by 30% up to six times if the
|
|
240
|
+
* estimate still lands over. A page whose first lines are themselves too long
|
|
241
|
+
* falls back to a character excerpt, which cannot be paged by line.
|
|
242
|
+
*/
|
|
243
|
+
function trimFirstPage(
|
|
244
|
+
selected: readonly string[],
|
|
245
|
+
totalLines: number,
|
|
246
|
+
selectedText: string,
|
|
247
|
+
tokens: number,
|
|
248
|
+
maxTokens: number,
|
|
249
|
+
path: string,
|
|
250
|
+
): RenderedRead {
|
|
251
|
+
const charsPerToken = Math.max(0.5, selectedText.length / Math.max(1, tokens))
|
|
252
|
+
const fits = (text: string) => text.length / charsPerToken <= maxTokens
|
|
253
|
+
|
|
254
|
+
let keep = Math.max(
|
|
255
|
+
1,
|
|
256
|
+
Math.min(
|
|
257
|
+
selected.length,
|
|
258
|
+
Math.floor(((selected.length * maxTokens) / Math.max(1, tokens)) * TRIM_TARGET),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
let page = selected.slice(0, keep).join('\n')
|
|
262
|
+
for (let i = 0; i < 6; i++) {
|
|
263
|
+
if (fits(page) || keep <= 1) break
|
|
264
|
+
keep = Math.max(1, Math.floor(keep * 0.7))
|
|
265
|
+
page = selected.slice(0, keep).join('\n')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tail = 'Do NOT answer from this page alone if the answer may be further in the file.]'
|
|
269
|
+
if (!fits(page) || page.trim() === '') {
|
|
270
|
+
let chars = Math.max(1, Math.floor(maxTokens * charsPerToken * TRIM_TARGET))
|
|
271
|
+
let excerpt = selectedText.slice(0, chars)
|
|
272
|
+
for (let i = 0; i < 6 && !fits(excerpt); i++) {
|
|
273
|
+
chars = Math.max(1, Math.floor(chars * 0.7))
|
|
274
|
+
excerpt = selectedText.slice(0, chars)
|
|
275
|
+
}
|
|
276
|
+
const last = excerpt.charCodeAt(excerpt.length - 1)
|
|
277
|
+
if (last >= 0xd800 && last <= 0xdbff) excerpt = excerpt.slice(0, -1)
|
|
278
|
+
const shown = excerpt.split('\n').length
|
|
279
|
+
return {
|
|
280
|
+
output: `${numbered(excerpt.split('\n'))}\n\n[Truncated: PARTIAL view — ${path}: showing the first ${excerpt.length} of ${selectedText.length} characters (${tokens} tokens, cap ${maxTokens}); this file has very long lines and cannot be paged by line. Use grep to find a specific section, or read with offset and limit. ${tail}`,
|
|
281
|
+
totalLines,
|
|
282
|
+
returnedLines: shown,
|
|
283
|
+
partial: true,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
output: `${numbered(selected.slice(0, keep))}\n\n[Truncated: PARTIAL view — ${path}: showing lines 1-${keep} of ${totalLines} total (${tokens} tokens, cap ${maxTokens}). Call read with offset=${keep} limit=${keep} for the next page, or grep to find a specific section. ${tail}`,
|
|
289
|
+
totalLines,
|
|
290
|
+
returnedLines: keep,
|
|
291
|
+
partial: true,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function numbered(lines: readonly string[]): string {
|
|
296
|
+
return lines.map((line, i) => `${i + 1}\t${line}`).join('\n')
|
|
297
|
+
}
|