@namzu/sdk 56.0.1 → 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 (34) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/runtime/query/executor.d.ts +9 -0
  3. package/dist/runtime/query/executor.d.ts.map +1 -1
  4. package/dist/runtime/query/executor.js +63 -3
  5. package/dist/runtime/query/executor.js.map +1 -1
  6. package/dist/tools/builtins/content-fingerprint.d.ts +12 -0
  7. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
  8. package/dist/tools/builtins/content-fingerprint.js +14 -0
  9. package/dist/tools/builtins/content-fingerprint.js.map +1 -1
  10. package/dist/tools/builtins/edit.d.ts.map +1 -1
  11. package/dist/tools/builtins/edit.js +19 -16
  12. package/dist/tools/builtins/edit.js.map +1 -1
  13. package/dist/tools/builtins/stale-before-review.d.ts +18 -0
  14. package/dist/tools/builtins/stale-before-review.d.ts.map +1 -0
  15. package/dist/tools/builtins/stale-before-review.js +42 -0
  16. package/dist/tools/builtins/stale-before-review.js.map +1 -0
  17. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  18. package/dist/tools/builtins/write-file.js +4 -0
  19. package/dist/tools/builtins/write-file.js.map +1 -1
  20. package/dist/tools/defineTool.d.ts +2 -0
  21. package/dist/tools/defineTool.d.ts.map +1 -1
  22. package/dist/tools/defineTool.js +1 -0
  23. package/dist/tools/defineTool.js.map +1 -1
  24. package/dist/types/tool/index.d.ts +16 -0
  25. package/dist/types/tool/index.d.ts.map +1 -1
  26. package/dist/types/tool/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/runtime/query/executor.ts +64 -3
  29. package/src/tools/builtins/content-fingerprint.ts +20 -0
  30. package/src/tools/builtins/edit.ts +18 -15
  31. package/src/tools/builtins/stale-before-review.ts +45 -0
  32. package/src/tools/builtins/write-file.ts +5 -0
  33. package/src/tools/defineTool.ts +3 -0
  34. package/src/types/tool/index.ts +18 -0
@@ -17,6 +17,7 @@ import {
17
17
  import { withFileMutationLock } from './file-mutation-lock.js'
18
18
  import { jsonStringEscapes } from './json-string-hint.js'
19
19
  import { lineDiff } from './line-diff.js'
20
+ import { staleBeforeReview } from './stale-before-review.js'
20
21
 
21
22
  /**
22
23
  * Two schemas, on purpose.
@@ -351,11 +352,13 @@ type EditAdmission =
351
352
  * 1. A file the session never read is refused. The model is about to rewrite
352
353
  * text it has not seen, which is the commonest source of a wrong patch. A
353
354
  * host that supplies no tracker has no ledger to ask and is not refused.
354
- * 2. A file that moved since the read is refused UNLESS every operation is a
355
- * text replacement that still applies to the current body — uniquely,
356
- * unless `replace_all` was asked for. A formatter run between the read and
357
- * the edit then costs nothing. An insertion by line number is never
358
- * recovered: the line it named may now be a different line.
355
+ * 2. A file that moved since the read is refused, whatever the operations are.
356
+ * There is no recovery: a replacement that "still applies" to the moved body
357
+ * can apply somewhere the model never meant (the read said `a = 1`, the file
358
+ * now says `a = 100`, and the replacement of `a = 1` by `a = 2` lands inside
359
+ * the new number and writes `a = 200`). Only the body the model read can
360
+ * show where its text belongs, and the ledger keeps a fingerprint of it, not
361
+ * the body. The refusal tells the model to read the file again.
359
362
  */
360
363
  function admitAgainstRead(
361
364
  context: ToolContext,
@@ -373,19 +376,16 @@ function admitAgainstRead(
373
376
  }
374
377
  }
375
378
  const seen = tracker?.fingerprint?.(key)
376
- const applied = applyEdit(content, operations)
377
379
  if (seen !== undefined && seen !== fingerprintContent(content)) {
378
- const recoverable = applied.success && operations.every((o) => o.operation === 'replace')
379
- if (!recoverable) {
380
- recordDrift(context, key)
381
- return {
382
- success: false,
383
- output: '',
384
- error: staleFileError(key),
385
- facts: { kind: 'failure', reason: 'changed_on_disk', subject: key },
386
- }
380
+ recordDrift(context, key)
381
+ return {
382
+ success: false,
383
+ output: '',
384
+ error: staleFileError(key),
385
+ facts: { kind: 'failure', reason: 'changed_on_disk', subject: key },
387
386
  }
388
387
  }
388
+ const applied = applyEdit(content, operations)
389
389
  return { success: true, result: applied }
390
390
  }
391
391
 
@@ -444,6 +444,9 @@ export const EditTool = defineTool({
444
444
  // Declared so a path outside the turn's roots can be reviewed before the
445
445
  // call runs, rather than refused after. See `ToolDefinition.pathArgument`.
446
446
  pathArgument: 'path',
447
+ // A file that moved since the read is refused whatever the replacement is,
448
+ // so the person is not asked to approve one that cannot run.
449
+ refuseBeforeReview: (input: EditInput, context) => staleBeforeReview(context, input.path, 'edit'),
447
450
  // The classifier judges which file changes and in what way, in a line.
448
451
  classifierInput: (input: EditInput) => {
449
452
  if (input.edits !== undefined) {
@@ -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.