@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/registry/tool/screen.d.ts.map +1 -1
  3. package/dist/registry/tool/screen.js +3 -1
  4. package/dist/registry/tool/screen.js.map +1 -1
  5. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  6. package/dist/runtime/query/executor/tool-call-admission.js +6 -1
  7. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  8. package/dist/runtime/query/executor.d.ts +9 -0
  9. package/dist/runtime/query/executor.d.ts.map +1 -1
  10. package/dist/runtime/query/executor.js +63 -3
  11. package/dist/runtime/query/executor.js.map +1 -1
  12. package/dist/runtime/query/file-evidence-replay.js +7 -3
  13. package/dist/runtime/query/file-evidence-replay.js.map +1 -1
  14. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  15. package/dist/runtime/query/review-policy.js +13 -1
  16. package/dist/runtime/query/review-policy.js.map +1 -1
  17. package/dist/tools/builtins/content-fingerprint.d.ts +12 -0
  18. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
  19. package/dist/tools/builtins/content-fingerprint.js +14 -0
  20. package/dist/tools/builtins/content-fingerprint.js.map +1 -1
  21. package/dist/tools/builtins/edit.d.ts.map +1 -1
  22. package/dist/tools/builtins/edit.js +19 -16
  23. package/dist/tools/builtins/edit.js.map +1 -1
  24. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  25. package/dist/tools/builtins/read-file.js +19 -6
  26. package/dist/tools/builtins/read-file.js.map +1 -1
  27. package/dist/tools/builtins/read-render.d.ts +3 -0
  28. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  29. package/dist/tools/builtins/read-render.js +40 -9
  30. package/dist/tools/builtins/read-render.js.map +1 -1
  31. package/dist/tools/builtins/stale-before-review.d.ts +18 -0
  32. package/dist/tools/builtins/stale-before-review.d.ts.map +1 -0
  33. package/dist/tools/builtins/stale-before-review.js +42 -0
  34. package/dist/tools/builtins/stale-before-review.js.map +1 -0
  35. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  36. package/dist/tools/builtins/write-file.js +4 -0
  37. package/dist/tools/builtins/write-file.js.map +1 -1
  38. package/dist/tools/defineTool.d.ts +2 -0
  39. package/dist/tools/defineTool.d.ts.map +1 -1
  40. package/dist/tools/defineTool.js +1 -0
  41. package/dist/tools/defineTool.js.map +1 -1
  42. package/dist/types/tool/index.d.ts +16 -0
  43. package/dist/types/tool/index.d.ts.map +1 -1
  44. package/dist/types/tool/index.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/registry/tool/screen.ts +3 -1
  47. package/src/runtime/query/executor/tool-call-admission.ts +5 -1
  48. package/src/runtime/query/executor.ts +64 -3
  49. package/src/runtime/query/file-evidence-replay.ts +6 -3
  50. package/src/runtime/query/review-policy.ts +19 -1
  51. package/src/tools/builtins/content-fingerprint.ts +20 -0
  52. package/src/tools/builtins/edit.ts +18 -15
  53. package/src/tools/builtins/read-file.ts +18 -7
  54. package/src/tools/builtins/read-render.ts +40 -9
  55. package/src/tools/builtins/stale-before-review.ts +45 -0
  56. package/src/tools/builtins/write-file.ts +5 -0
  57. package/src/tools/defineTool.ts +3 -0
  58. 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
- const { start, end } = resolveReadWindow(input, lines.length)
58
- const selected = lines.slice(start, end)
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, lines.length),
62
- totalLines: lines.length,
84
+ output: numbered + partialViewNotice(start, selected.length, lineCount),
85
+ totalLines: lineCount,
63
86
  returnedLines: selected.length,
64
- partial: selected.length < lines.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 { start, end } = resolveReadWindow(input, lines.length)
204
- const selected = lines.slice(start, end)
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, lines.length, selectedText, tokens, maxTokens, options.path),
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 ?? ''
@@ -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 }
@@ -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.