@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.
- package/CHANGELOG.md +10 -0
- 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/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/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/runtime/query/executor.ts +64 -3
- package/src/tools/builtins/content-fingerprint.ts +20 -0
- package/src/tools/builtins/edit.ts +18 -15
- 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
|
@@ -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
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* the
|
|
358
|
-
*
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 ?? ''
|
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.
|