@namzu/sdk 56.0.0 → 56.1.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 +18 -0
- package/dist/registry/tool/screen.d.ts.map +1 -1
- package/dist/registry/tool/screen.js +3 -1
- package/dist/registry/tool/screen.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +6 -1
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +9 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +63 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-replay.js +7 -3
- package/dist/runtime/query/file-evidence-replay.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +13 -1
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +12 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +14 -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 +19 -16
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +19 -6
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +3 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +40 -9
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/stale-before-review.d.ts +18 -0
- package/dist/tools/builtins/stale-before-review.d.ts.map +1 -0
- package/dist/tools/builtins/stale-before-review.js +42 -0
- package/dist/tools/builtins/stale-before-review.js.map +1 -0
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +4 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/types/tool/index.d.ts +16 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +1 -1
- package/src/registry/tool/screen.ts +3 -1
- package/src/runtime/query/executor/tool-call-admission.ts +5 -1
- package/src/runtime/query/executor.ts +64 -3
- package/src/runtime/query/file-evidence-replay.ts +6 -3
- package/src/runtime/query/review-policy.ts +19 -1
- package/src/tools/builtins/content-fingerprint.ts +20 -0
- package/src/tools/builtins/edit.ts +18 -15
- package/src/tools/builtins/read-file.ts +18 -7
- package/src/tools/builtins/read-render.ts +40 -9
- package/src/tools/builtins/stale-before-review.ts +45 -0
- package/src/tools/builtins/write-file.ts +5 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/types/tool/index.ts +18 -0
|
@@ -54,14 +54,37 @@ export interface RenderedRead {
|
|
|
54
54
|
*/
|
|
55
55
|
export function renderNumberedRead(content: string, input: ReadWindowRequest): RenderedRead {
|
|
56
56
|
const lines = content.split('\n')
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// One definition of a file's length for the header, the notice and the
|
|
58
|
+
// past-end warning: a final newline ends a line, it does not start another.
|
|
59
|
+
const lineCount = countFileLines(content)
|
|
60
|
+
const { start, end } = resolveReadWindow(input, lineCount)
|
|
61
|
+
// A file with no text in it, or a window that opens after its last line,
|
|
62
|
+
// has nothing to number. Say that in words instead of printing a numbered
|
|
63
|
+
// empty line or a "PARTIAL view" range that runs backwards.
|
|
64
|
+
if (content === '') {
|
|
65
|
+
return {
|
|
66
|
+
output: 'Warning: the file exists but is empty.',
|
|
67
|
+
totalLines: 0,
|
|
68
|
+
returnedLines: 0,
|
|
69
|
+
partial: false,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (start >= lineCount) {
|
|
73
|
+
return {
|
|
74
|
+
output: `Warning: the file is shorter than the requested start (line ${start + 1}). It has ${lineCount} ${lineCount === 1 ? 'line' : 'lines'}.`,
|
|
75
|
+
totalLines: lineCount,
|
|
76
|
+
returnedLines: 0,
|
|
77
|
+
// Nothing of the file was shown, so this must never stand in for it.
|
|
78
|
+
partial: true,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const selected = lines.slice(start, Math.min(end, lineCount))
|
|
59
82
|
const numbered = selected.map((line, i) => `${start + i + 1}\t${line}`).join('\n')
|
|
60
83
|
return {
|
|
61
|
-
output: numbered + partialViewNotice(start, selected.length,
|
|
62
|
-
totalLines:
|
|
84
|
+
output: numbered + partialViewNotice(start, selected.length, lineCount),
|
|
85
|
+
totalLines: lineCount,
|
|
63
86
|
returnedLines: selected.length,
|
|
64
|
-
partial: selected.length <
|
|
87
|
+
partial: selected.length < lineCount,
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
|
|
@@ -85,6 +108,13 @@ function partialViewNotice(start: number, returned: number, total: number): stri
|
|
|
85
108
|
].join('\n')
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
/** The number of lines a file has: a final newline ends a line, it does not start another. */
|
|
112
|
+
export function countFileLines(content: string): number {
|
|
113
|
+
if (content === '') return 0
|
|
114
|
+
const parts = content.split('\n').length
|
|
115
|
+
return content.endsWith('\n') ? parts - 1 : parts
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
export function resolveReadWindow(
|
|
89
119
|
input: ReadWindowRequest,
|
|
90
120
|
totalLines: number,
|
|
@@ -153,7 +183,7 @@ export function estimateReadTokens(text: string, ext: string): number {
|
|
|
153
183
|
return Math.round(text.length / perToken)
|
|
154
184
|
}
|
|
155
185
|
|
|
156
|
-
function humanBytes(n: number): string {
|
|
186
|
+
export function humanBytes(n: number): string {
|
|
157
187
|
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`
|
|
158
188
|
if (n >= 1024) return `${Math.round(n / 1024)} KiB`
|
|
159
189
|
return `${n} B`
|
|
@@ -200,8 +230,9 @@ export function renderCappedRead(
|
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
const lines = content.split('\n')
|
|
203
|
-
const
|
|
204
|
-
const
|
|
233
|
+
const lineCount = countFileLines(content)
|
|
234
|
+
const { start, end } = resolveReadWindow(input, lineCount)
|
|
235
|
+
const selected = lines.slice(start, Math.min(end, lineCount))
|
|
205
236
|
const selectedText = selected.join('\n')
|
|
206
237
|
|
|
207
238
|
const selectedBytes = Buffer.byteLength(selectedText, 'utf8')
|
|
@@ -226,7 +257,7 @@ export function renderCappedRead(
|
|
|
226
257
|
}
|
|
227
258
|
return {
|
|
228
259
|
ok: true,
|
|
229
|
-
rendered: trimFirstPage(selected,
|
|
260
|
+
rendered: trimFirstPage(selected, lineCount, selectedText, tokens, maxTokens, options.path),
|
|
230
261
|
}
|
|
231
262
|
}
|
|
232
263
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import type { ToolContext } from '../../types/tool/index.js'
|
|
4
|
+
import type { ToolFacts } from '../../types/tool/presentation.js'
|
|
5
|
+
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
6
|
+
import { movedSinceRead, staleFileError } from './content-fingerprint.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The refusal a file mutation would give for a file that moved on disk since
|
|
10
|
+
* the session read it, asked before a person is.
|
|
11
|
+
*
|
|
12
|
+
* `edit` and `write` refuse such a call when they run. By then the person has
|
|
13
|
+
* already been shown a change and said yes to it, so the answer is asked here
|
|
14
|
+
* too, ahead of the review. It looks at the same body the tool will (the host
|
|
15
|
+
* file, or the sandbox's) and says nothing when it cannot tell: a path that is
|
|
16
|
+
* missing or outside the roots, an unreadable file, or a session that never
|
|
17
|
+
* read the path. Those keep their own answers at run time.
|
|
18
|
+
*/
|
|
19
|
+
export async function staleBeforeReview(
|
|
20
|
+
context: ToolContext,
|
|
21
|
+
path: string,
|
|
22
|
+
operation: 'edit' | 'write',
|
|
23
|
+
): Promise<{ readonly error: string; readonly facts: ToolFacts } | undefined> {
|
|
24
|
+
const tracker = context.fileReadTracker
|
|
25
|
+
if (!tracker) return undefined
|
|
26
|
+
let key: string
|
|
27
|
+
let current: string
|
|
28
|
+
try {
|
|
29
|
+
if (context.sandbox) {
|
|
30
|
+
key = path
|
|
31
|
+
current = (await context.sandbox.readFile(path)).toString('utf-8')
|
|
32
|
+
} else {
|
|
33
|
+
key = await resolveWithinAnyReal(toolRoots(context), path)
|
|
34
|
+
current = await readFile(key, 'utf-8')
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
if (!movedSinceRead(tracker, key, current)) return undefined
|
|
40
|
+
tracker.recordDriftObserved?.(key)
|
|
41
|
+
return {
|
|
42
|
+
error: staleFileError(key, operation),
|
|
43
|
+
facts: { kind: 'failure', reason: 'changed_on_disk', subject: key },
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -11,6 +11,7 @@ import { fingerprintContent, staleFileError } from './content-fingerprint.js'
|
|
|
11
11
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
12
12
|
import { fileWriteResult } from './file-write-result.js'
|
|
13
13
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
14
|
+
import { staleBeforeReview } from './stale-before-review.js'
|
|
14
15
|
|
|
15
16
|
const inputSchema = z
|
|
16
17
|
.object({
|
|
@@ -94,6 +95,10 @@ export const WriteFileTool = defineTool({
|
|
|
94
95
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
95
96
|
// call runs, rather than refused after. See `ToolDefinition.pathArgument`.
|
|
96
97
|
pathArgument: 'path',
|
|
98
|
+
// An overwrite of a file that moved since the read is refused when it runs,
|
|
99
|
+
// so the person is not asked to approve one that cannot.
|
|
100
|
+
refuseBeforeReview: (input: WriteInput, context) =>
|
|
101
|
+
staleBeforeReview(context, input.path, 'write'),
|
|
97
102
|
// The classifier judges where a file is written and how much, not the body.
|
|
98
103
|
classifierInput: (input: WriteInput) => {
|
|
99
104
|
const body = input.content ?? input.newStr ?? ''
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
83
83
|
commandDialect?: ToolDefinition['commandDialect']
|
|
84
84
|
/** The argument holding a filesystem path; see {@link ToolDefinition.pathArgument}. */
|
|
85
85
|
pathArgument?: string
|
|
86
|
+
/** A refusal known before any reviewer is asked; see {@link ToolDefinition.refuseBeforeReview}. */
|
|
87
|
+
refuseBeforeReview?: ToolDefinition<z.infer<S>>['refuseBeforeReview']
|
|
86
88
|
/** The argument holding a canonical URL; see {@link ToolDefinition.urlArgument}. */
|
|
87
89
|
urlArgument?: string
|
|
88
90
|
/** The argument asking to leave the sandbox; see {@link ToolDefinition.sandboxEscapeArgument}. */
|
|
@@ -146,6 +148,7 @@ export function defineTool<S extends z.ZodType>(
|
|
|
146
148
|
...(options.commandArgument !== undefined ? { commandArgument: options.commandArgument } : {}),
|
|
147
149
|
...(options.commandDialect !== undefined ? { commandDialect: options.commandDialect } : {}),
|
|
148
150
|
...(options.pathArgument !== undefined ? { pathArgument: options.pathArgument } : {}),
|
|
151
|
+
...(options.refuseBeforeReview ? { refuseBeforeReview: options.refuseBeforeReview } : {}),
|
|
149
152
|
...(options.urlArgument !== undefined ? { urlArgument: options.urlArgument } : {}),
|
|
150
153
|
...(options.sandboxEscapeArgument !== undefined
|
|
151
154
|
? { sandboxEscapeArgument: options.sandboxEscapeArgument }
|
package/src/types/tool/index.ts
CHANGED
|
@@ -1050,6 +1050,24 @@ export interface ToolDefinition<TInput = unknown> extends ToolPresentation<TInpu
|
|
|
1050
1050
|
* the safe direction to be wrong in.
|
|
1051
1051
|
*/
|
|
1052
1052
|
pathArgument?: string
|
|
1053
|
+
/**
|
|
1054
|
+
* A refusal that needs no person's answer: the call cannot run as it stands,
|
|
1055
|
+
* and the tool can already tell.
|
|
1056
|
+
*
|
|
1057
|
+
* Asked once the arguments are valid and BEFORE the call is put to a
|
|
1058
|
+
* reviewer, so a person is never asked to approve a change that would be
|
|
1059
|
+
* refused the moment they said yes. A file tool answers here when the file
|
|
1060
|
+
* moved on disk after the session read it. Returning `undefined` means no
|
|
1061
|
+
* objection; the call goes on to review and `execute`, which keep their own
|
|
1062
|
+
* checks, since the state may move again in between. A hook that throws is
|
|
1063
|
+
* read as no objection too.
|
|
1064
|
+
*/
|
|
1065
|
+
refuseBeforeReview?(
|
|
1066
|
+
input: TInput,
|
|
1067
|
+
context: ToolContext,
|
|
1068
|
+
): Promise<
|
|
1069
|
+
{ readonly error: string; readonly facts?: import('./presentation.js').ToolFacts } | undefined
|
|
1070
|
+
>
|
|
1053
1071
|
/**
|
|
1054
1072
|
* The argument that holds an absolute URL the tool has already
|
|
1055
1073
|
* canonicalised in its input schema — one spelling, no whitespace.
|