@namzu/sdk 52.4.0 → 52.6.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 +33 -0
- package/dist/authorization/gate.d.ts +11 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +35 -0
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/removal.js +11 -6
- package/dist/authorization/removal.js.map +1 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +30 -2
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-paths.d.ts +20 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -1
- package/dist/authorization/shell-paths.js +254 -52
- package/dist/authorization/shell-paths.js.map +1 -1
- package/dist/bridge/acp/server.d.ts +8 -0
- package/dist/bridge/acp/server.d.ts.map +1 -1
- package/dist/bridge/acp/server.js +7 -0
- package/dist/bridge/acp/server.js.map +1 -1
- package/dist/manager/resident/outbox.d.ts +4 -4
- package/dist/public-runtime.d.ts +2 -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 +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/classifier.d.ts +130 -0
- package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/classifier.js +244 -0
- package/dist/runtime/query/auto-review/classifier.js.map +1 -0
- package/dist/runtime/query/auto-review/index.d.ts +7 -0
- package/dist/runtime/query/auto-review/index.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/index.js +4 -0
- package/dist/runtime/query/auto-review/index.js.map +1 -0
- package/dist/runtime/query/auto-review/prompt.d.ts +20 -0
- package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/prompt.js +89 -0
- package/dist/runtime/query/auto-review/prompt.js.map +1 -0
- package/dist/runtime/query/auto-review/rules.d.ts +49 -0
- package/dist/runtime/query/auto-review/rules.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/rules.js +81 -0
- package/dist/runtime/query/auto-review/rules.js.map +1 -0
- package/dist/runtime/query/auto-review/transcript.d.ts +59 -0
- package/dist/runtime/query/auto-review/transcript.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/transcript.js +116 -0
- package/dist/runtime/query/auto-review/transcript.js.map +1 -0
- package/dist/runtime/query/auto-review/verdict.d.ts +21 -0
- package/dist/runtime/query/auto-review/verdict.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/verdict.js +45 -0
- package/dist/runtime/query/auto-review/verdict.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +7 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +26 -2
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.js +2 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +0 -9
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +11 -2
- 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 +39 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +43 -10
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +14 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +88 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +504 -11
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +2 -0
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/classifier-input.d.ts +9 -0
- package/dist/tools/builtins/classifier-input.d.ts.map +1 -0
- package/dist/tools/builtins/classifier-input.js +20 -0
- package/dist/tools/builtins/classifier-input.js.map +1 -0
- package/dist/tools/builtins/code-analysis.d.ts +2 -0
- package/dist/tools/builtins/code-analysis.d.ts.map +1 -1
- package/dist/tools/builtins/code-analysis.js +196 -119
- package/dist/tools/builtins/code-analysis.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +10 -10
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +30 -12
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit-match.d.ts +47 -0
- package/dist/tools/builtins/edit-match.d.ts.map +1 -0
- package/dist/tools/builtins/edit-match.js +262 -0
- package/dist/tools/builtins/edit-match.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +12 -0
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/monitor.d.ts.map +1 -1
- package/dist/tools/builtins/monitor.js +3 -0
- package/dist/tools/builtins/monitor.js.map +1 -1
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +3 -0
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +6 -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/acp/index.d.ts +7 -0
- package/dist/types/acp/index.d.ts.map +1 -1
- package/dist/types/code-analysis/index.d.ts +54 -3
- package/dist/types/code-analysis/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +45 -1
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.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/authorization/gate.ts +35 -0
- package/src/authorization/removal.ts +12 -6
- package/src/authorization/rules.ts +27 -2
- package/src/authorization/shell-paths.ts +264 -51
- package/src/bridge/acp/server.ts +20 -0
- package/src/public-runtime.ts +18 -0
- package/src/public-types.ts +11 -0
- package/src/runtime/query/auto-review/classifier.ts +404 -0
- package/src/runtime/query/auto-review/index.ts +23 -0
- package/src/runtime/query/auto-review/prompt.ts +97 -0
- package/src/runtime/query/auto-review/rules.ts +106 -0
- package/src/runtime/query/auto-review/transcript.ts +159 -0
- package/src/runtime/query/auto-review/verdict.ts +50 -0
- package/src/runtime/query/executor.ts +29 -2
- package/src/runtime/query/index.ts +2 -2
- package/src/runtime/query/iteration/phases/context.ts +17 -3
- package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
- package/src/runtime/query/path-escalation.ts +44 -9
- package/src/runtime/query/resume-pending.ts +14 -0
- package/src/runtime/query/review-policy.ts +591 -14
- package/src/tools/builtins/bash.ts +3 -0
- package/src/tools/builtins/classifier-input.ts +19 -0
- package/src/tools/builtins/code-analysis.ts +211 -119
- package/src/tools/builtins/edit-apply.ts +31 -12
- package/src/tools/builtins/edit-match.ts +274 -0
- package/src/tools/builtins/edit.ts +13 -0
- package/src/tools/builtins/monitor.ts +4 -0
- package/src/tools/builtins/web.ts +3 -0
- package/src/tools/builtins/write-file.ts +6 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/types/acp/index.ts +7 -0
- package/src/types/code-analysis/index.ts +51 -3
- package/src/types/hitl/index.ts +49 -1
- package/src/types/tool/index.ts +17 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible `old_string` matching for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* When an exact match of `old_string` against the file fails, a handful of
|
|
5
|
+
* near-misses are not the model being wrong about the text, only about one
|
|
6
|
+
* invisible or interchangeable detail: a curly quote where the file has a
|
|
7
|
+
* straight one, or a `\uXXXX` escape where the file has the character itself
|
|
8
|
+
* (or the reverse). This module recovers the real span the model meant, and
|
|
9
|
+
* carries the same transformation across to `new_string` so the replacement
|
|
10
|
+
* stays consistent with what it replaced.
|
|
11
|
+
*
|
|
12
|
+
* The strategies and their order are a faithful port of the reference client's
|
|
13
|
+
* file-edit resolver (the `lje`/`b`/`_`/`L`/`k`/`R`/`C`/`cDs`/`ipt`/`H`/`Q`/`N`
|
|
14
|
+
* functions in its shipped bundle), so an edit that lands there lands here:
|
|
15
|
+
*
|
|
16
|
+
* 1. exact
|
|
17
|
+
* 2. curly vs straight quotes, both directions
|
|
18
|
+
* 3. `\uXXXX` escape in `old_string`, character in the file
|
|
19
|
+
* 4. character in `old_string`, `\uXXXX` escape in the file
|
|
20
|
+
*
|
|
21
|
+
* The reference also expands a map of its own prompt delimiters (`<fnr>` →
|
|
22
|
+
* `<function_results>`, `\n\nH:` → `\n\nHuman:`, …). That is deliberately NOT
|
|
23
|
+
* ported: those tokens are artifacts of that client's own transcript format,
|
|
24
|
+
* not a mismatch a Namzu model produces against a real source file.
|
|
25
|
+
*
|
|
26
|
+
* Pure by construction, like `edit-apply`: every function here is from strings
|
|
27
|
+
* to strings, with no filesystem access.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const LEFT_SINGLE_QUOTE = '‘'
|
|
31
|
+
const RIGHT_SINGLE_QUOTE = '’'
|
|
32
|
+
const LEFT_DOUBLE_QUOTE = '“'
|
|
33
|
+
const RIGHT_DOUBLE_QUOTE = '”'
|
|
34
|
+
|
|
35
|
+
/** `old_string` carries a `\uXXXX` escape the file may hold as a character. */
|
|
36
|
+
const HAS_UNICODE_ESCAPE = /\\u[0-9a-fA-F]{4}/
|
|
37
|
+
/** `old_string` carries a non-ASCII character the file may hold as an escape. */
|
|
38
|
+
const HAS_NON_ASCII = /[\u0080-]/
|
|
39
|
+
|
|
40
|
+
/** Curly quotes to straight. Length-preserving: one character for one. */
|
|
41
|
+
function straightenQuotes(text: string): string {
|
|
42
|
+
return text
|
|
43
|
+
.replaceAll(LEFT_SINGLE_QUOTE, "'")
|
|
44
|
+
.replaceAll(RIGHT_SINGLE_QUOTE, "'")
|
|
45
|
+
.replaceAll(LEFT_DOUBLE_QUOTE, '"')
|
|
46
|
+
.replaceAll(RIGHT_DOUBLE_QUOTE, '"')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Replace each `\uXXXX` escape with its character, leaving an escaped
|
|
51
|
+
* backslash (`\\`) untouched so `\\u0041` stays the five characters it is
|
|
52
|
+
* rather than becoming a stray `A`.
|
|
53
|
+
*/
|
|
54
|
+
function decodeUnicodeEscapes(text: string): string {
|
|
55
|
+
return text.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (whole, backslashes, hex) =>
|
|
56
|
+
backslashes !== undefined ? whole : String.fromCharCode(Number.parseInt(hex, 16)),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The length `text` would have if every non-ASCII character were a `\uXXXX`. */
|
|
61
|
+
function escapedLength(text: string): number {
|
|
62
|
+
let nonAscii = 0
|
|
63
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) >= 128) nonAscii++
|
|
64
|
+
return text.length + 5 * nonAscii
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Consecutive backslashes immediately before index `at`. */
|
|
68
|
+
function trailingBackslashes(text: string, at: number): number {
|
|
69
|
+
let count = 0
|
|
70
|
+
while (text[at - 1 - count] === '\\') count++
|
|
71
|
+
return count
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Escape a literal string so it matches itself inside a `RegExp`. */
|
|
75
|
+
function escapeRegExp(text: string): string {
|
|
76
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A pattern that matches `model` where every non-ASCII character appears as its
|
|
81
|
+
* `\uXXXX` escape (case-insensitive in the hex letters) and every ASCII
|
|
82
|
+
* character appears literally. Used to find, in a file written with escapes,
|
|
83
|
+
* the span a model wrote with the characters themselves.
|
|
84
|
+
*/
|
|
85
|
+
function buildEscapeTolerantPattern(model: string): string {
|
|
86
|
+
let pattern = ''
|
|
87
|
+
for (let i = 0; i < model.length; i++) {
|
|
88
|
+
const code = model.charCodeAt(i)
|
|
89
|
+
if (code >= 128) {
|
|
90
|
+
pattern += '\\\\u'
|
|
91
|
+
for (const digit of code.toString(16).padStart(4, '0'))
|
|
92
|
+
pattern += digit >= 'a' ? `[${digit}${digit.toUpperCase()}]` : digit
|
|
93
|
+
} else {
|
|
94
|
+
// biome-ignore lint/style/noNonNullAssertion: index < length
|
|
95
|
+
pattern += escapeRegExp(model[i]!)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return pattern
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find, in `file`, the escaped form of `model` — where `model` is written with
|
|
103
|
+
* real non-ASCII characters and `file` holds them as `\uXXXX`. Returns the
|
|
104
|
+
* matched (escaped) substring, or null when there is no such span, when the
|
|
105
|
+
* pattern would be ambiguous, or when a matched `\u` is itself escaped by an
|
|
106
|
+
* odd run of backslashes (a literal `\u`, not an escape).
|
|
107
|
+
*/
|
|
108
|
+
function findEscapedForm(model: string, file: string): string | null {
|
|
109
|
+
// A model ending in an odd number of backslashes cannot be the clean prefix
|
|
110
|
+
// of an escape sequence; bail rather than build a pattern that misreads it.
|
|
111
|
+
if (trailingBackslashes(model, model.length) % 2 === 1) return null
|
|
112
|
+
|
|
113
|
+
// Offsets, within the escaped match, whose backslash parity must be checked
|
|
114
|
+
// so a literal `\u` in the file is not mistaken for an escape.
|
|
115
|
+
const sensitiveOffsets: number[] = model[0] === '\\' ? [0] : []
|
|
116
|
+
let offset = 0
|
|
117
|
+
for (let i = 0; i < model.length; i++) {
|
|
118
|
+
if (model.charCodeAt(i) >= 128) {
|
|
119
|
+
sensitiveOffsets.push(offset)
|
|
120
|
+
offset += 6
|
|
121
|
+
} else {
|
|
122
|
+
offset += 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let regex: RegExp
|
|
127
|
+
try {
|
|
128
|
+
regex = new RegExp(buildEscapeTolerantPattern(model), 'g')
|
|
129
|
+
} catch {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
for (let match = regex.exec(file); match !== null; match = regex.exec(file)) {
|
|
133
|
+
const at = match.index
|
|
134
|
+
if (sensitiveOffsets.every((o) => trailingBackslashes(file, at + o) % 2 === 0)) {
|
|
135
|
+
return file.indexOf(match[0]) === at ? match[0] : null
|
|
136
|
+
}
|
|
137
|
+
regex.lastIndex = at + 1
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The span of `content` that `oldString` refers to, recovering it across a
|
|
144
|
+
* quote- or escape-only difference, or null when nothing matches even then.
|
|
145
|
+
*
|
|
146
|
+
* The returned string is a real substring of `content` (or its escaped form),
|
|
147
|
+
* so the caller can run the ordinary uniqueness check and splice against it.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveOldString(content: string, oldString: string): string | null {
|
|
150
|
+
if (content.includes(oldString)) return oldString
|
|
151
|
+
|
|
152
|
+
const straight = straightenQuotes(oldString)
|
|
153
|
+
const quoteIndex = straightenQuotes(content).indexOf(straight)
|
|
154
|
+
if (quoteIndex !== -1) return content.substring(quoteIndex, quoteIndex + oldString.length)
|
|
155
|
+
|
|
156
|
+
if (HAS_UNICODE_ESCAPE.test(oldString)) {
|
|
157
|
+
const decoded = decodeUnicodeEscapes(oldString)
|
|
158
|
+
if (decoded !== oldString && content.includes(decoded)) return decoded
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (HAS_NON_ASCII.test(oldString)) {
|
|
162
|
+
if (escapedLength(oldString) > content.length || !content.includes('\\u')) return null
|
|
163
|
+
return findEscapedForm(oldString, content)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Is `char` at `index` opening a quote (preceded by a boundary) rather than closing one? */
|
|
170
|
+
function isOpeningPosition(chars: readonly string[], index: number): boolean {
|
|
171
|
+
if (index === 0) return true
|
|
172
|
+
const before = chars[index - 1]
|
|
173
|
+
return (
|
|
174
|
+
before === ' ' ||
|
|
175
|
+
before === '\t' ||
|
|
176
|
+
before === '\n' ||
|
|
177
|
+
before === '\r' ||
|
|
178
|
+
before === '(' ||
|
|
179
|
+
before === '[' ||
|
|
180
|
+
before === '{' ||
|
|
181
|
+
before === '—' ||
|
|
182
|
+
before === '–'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Apply curly double quotes to `text`, choosing opening vs closing by context. */
|
|
187
|
+
function curlifyDoubleQuotes(text: string): string {
|
|
188
|
+
const chars = [...text]
|
|
189
|
+
return chars
|
|
190
|
+
.map((char, index) =>
|
|
191
|
+
char === '"'
|
|
192
|
+
? isOpeningPosition(chars, index)
|
|
193
|
+
? LEFT_DOUBLE_QUOTE
|
|
194
|
+
: RIGHT_DOUBLE_QUOTE
|
|
195
|
+
: char,
|
|
196
|
+
)
|
|
197
|
+
.join('')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Apply curly single quotes to `text`; an apostrophe between letters is always closing. */
|
|
201
|
+
function curlifySingleQuotes(text: string): string {
|
|
202
|
+
const chars = [...text]
|
|
203
|
+
return chars
|
|
204
|
+
.map((char, index) => {
|
|
205
|
+
if (char !== "'") return char
|
|
206
|
+
const before = index > 0 ? chars[index - 1] : undefined
|
|
207
|
+
const after = index < chars.length - 1 ? chars[index + 1] : undefined
|
|
208
|
+
const betweenLetters =
|
|
209
|
+
before !== undefined && /\p{L}/u.test(before) && after !== undefined && /\p{L}/u.test(after)
|
|
210
|
+
if (betweenLetters) return RIGHT_SINGLE_QUOTE
|
|
211
|
+
return isOpeningPosition(chars, index) ? LEFT_SINGLE_QUOTE : RIGHT_SINGLE_QUOTE
|
|
212
|
+
})
|
|
213
|
+
.join('')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Carry the transformation that matched `oldString` across to `newString`, so
|
|
218
|
+
* the replacement matches the span it lands in rather than the literal the
|
|
219
|
+
* model typed.
|
|
220
|
+
*
|
|
221
|
+
* `resolved` is what {@link resolveOldString} returned — equal to `oldString`
|
|
222
|
+
* for an exact match (then `newString` is returned unchanged), and otherwise
|
|
223
|
+
* the real span, which tells us which strategy fired.
|
|
224
|
+
*/
|
|
225
|
+
export function reconcileNewString(oldString: string, resolved: string, newString: string): string {
|
|
226
|
+
if (resolved === oldString) return newString
|
|
227
|
+
|
|
228
|
+
// character in old, escape in file: re-escape new's non-ASCII to the file's
|
|
229
|
+
// hex spelling, matching the letter-case the file used.
|
|
230
|
+
if (
|
|
231
|
+
HAS_NON_ASCII.test(oldString) &&
|
|
232
|
+
escapedLength(oldString) === resolved.length &&
|
|
233
|
+
findEscapedForm(oldString, resolved) !== null
|
|
234
|
+
) {
|
|
235
|
+
const perChar = new Map<number, string>()
|
|
236
|
+
let upper = 0
|
|
237
|
+
let lower = 0
|
|
238
|
+
for (let oldIndex = 0, resolvedIndex = 0; oldIndex < oldString.length; oldIndex++) {
|
|
239
|
+
const code = oldString.charCodeAt(oldIndex)
|
|
240
|
+
if (code >= 128) {
|
|
241
|
+
const hex = resolved.slice(resolvedIndex + 2, resolvedIndex + 6)
|
|
242
|
+
perChar.set(code, hex)
|
|
243
|
+
for (const digit of hex)
|
|
244
|
+
if (digit >= 'a' && digit <= 'f') lower++
|
|
245
|
+
else if (digit >= 'A' && digit <= 'F') upper++
|
|
246
|
+
resolvedIndex += 6
|
|
247
|
+
} else {
|
|
248
|
+
resolvedIndex += 1
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return newString.replace(/[\u0080-]/g, (char) => {
|
|
252
|
+
const code = char.charCodeAt(0)
|
|
253
|
+
const known = perChar.get(code)
|
|
254
|
+
if (known !== undefined) return `\\u${known}`
|
|
255
|
+
const hex = code.toString(16).padStart(4, '0')
|
|
256
|
+
return `\\u${upper > lower ? hex.toUpperCase() : hex}`
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// escape in old, character in file: decode new's escapes to match.
|
|
261
|
+
if (HAS_UNICODE_ESCAPE.test(oldString) && decodeUnicodeEscapes(oldString) === resolved) {
|
|
262
|
+
return decodeUnicodeEscapes(newString)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// quote difference: re-apply the file's curly quotes to new, by kind.
|
|
266
|
+
const hasCurlyDouble =
|
|
267
|
+
resolved.includes(LEFT_DOUBLE_QUOTE) || resolved.includes(RIGHT_DOUBLE_QUOTE)
|
|
268
|
+
const hasCurlySingle =
|
|
269
|
+
resolved.includes(LEFT_SINGLE_QUOTE) || resolved.includes(RIGHT_SINGLE_QUOTE)
|
|
270
|
+
let reconciled = newString
|
|
271
|
+
if (hasCurlyDouble) reconciled = curlifyDoubleQuotes(reconciled)
|
|
272
|
+
if (hasCurlySingle) reconciled = curlifySingleQuotes(reconciled)
|
|
273
|
+
return reconciled
|
|
274
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { ToolContext, ToolResult } from '../../types/tool/index.js'
|
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
6
|
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
7
7
|
import { atomicWriteFile } from './atomic-write-file.js'
|
|
8
|
+
import { clip, describeChange } from './classifier-input.js'
|
|
8
9
|
import { fingerprintContent, staleFileError, unreadFileError } from './content-fingerprint.js'
|
|
9
10
|
import { type NormalizedEditInput, applyEdit, normalizeEditInput } from './edit-apply.js'
|
|
10
11
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
@@ -393,6 +394,18 @@ export const EditTool = defineTool({
|
|
|
393
394
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
394
395
|
// call runs, rather than refused after. See `ToolDefinition.pathArgument`.
|
|
395
396
|
pathArgument: 'path',
|
|
397
|
+
// The classifier judges which file changes and in what way, in a line.
|
|
398
|
+
classifierInput: (input: EditInput) => {
|
|
399
|
+
if (input.edits !== undefined) {
|
|
400
|
+
const shown = input.edits.slice(0, 5).map((e) => describeChange(e.old_string, e.new_string))
|
|
401
|
+
const more =
|
|
402
|
+
input.edits.length > shown.length ? `; ${input.edits.length - shown.length} more` : ''
|
|
403
|
+
return clip(`${input.path}: ${input.edits.length} edits: ${shown.join('; ')}${more}`, 1500)
|
|
404
|
+
}
|
|
405
|
+
const removed = input.old_string ?? input.oldStr
|
|
406
|
+
const added = input.new_string ?? input.newStr
|
|
407
|
+
return clip(`${input.path}: ${describeChange(removed, added)}`, 1500)
|
|
408
|
+
},
|
|
396
409
|
permissions: ['file_write'],
|
|
397
410
|
readOnly: false,
|
|
398
411
|
destructive: false,
|
|
@@ -178,6 +178,10 @@ export function createMonitorTool(options: MonitorToolOptions = {}) {
|
|
|
178
178
|
// has no `command`, so a rule written for commands has nothing to match
|
|
179
179
|
// and cannot allow it; `requiresApproval` below is what asks.
|
|
180
180
|
commandArgument: 'command',
|
|
181
|
+
classifierInput: (input: MonitorInput) =>
|
|
182
|
+
input.command !== undefined
|
|
183
|
+
? `[long-running watch] ${input.command}`
|
|
184
|
+
: `[long-running WebSocket watch] ${JSON.stringify(input.ws)}`,
|
|
181
185
|
commandDialect: bashToolDialect,
|
|
182
186
|
// `network_access` too: a WebSocket call reaches the network. The gate
|
|
183
187
|
// reads a tool's `category`, not this list, to route a call, so declaring
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { WebFetchRefusedError } from '../../connector/web/types.js'
|
|
12
12
|
import type { ToolContext } from '../../types/tool/index.js'
|
|
13
13
|
import { defineTool } from '../defineTool.js'
|
|
14
|
+
import { clip } from './classifier-input.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The two web tools, and the reason they are one file.
|
|
@@ -151,6 +152,8 @@ export const WebFetchTool = defineTool({
|
|
|
151
152
|
// preset — handing outbound reach to runs whose sandbox was never asked
|
|
152
153
|
// to confine the network.
|
|
153
154
|
category: 'network',
|
|
155
|
+
// The classifier judges the address and what is asked of it.
|
|
156
|
+
classifierInput: (input) => `${clip(input.url, 1000)}: ${clip(input.prompt, 300)}`,
|
|
154
157
|
permissions: ['network_access'],
|
|
155
158
|
// Reads a page and changes nothing HERE. That is not the same as
|
|
156
159
|
// harmless — a GET can still act on a remote system — which is why the
|
|
@@ -6,6 +6,7 @@ import type { ToolFacts } from '../../types/tool/presentation.js'
|
|
|
6
6
|
import { defineTool } from '../defineTool.js'
|
|
7
7
|
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
8
8
|
import { atomicWriteFile } from './atomic-write-file.js'
|
|
9
|
+
import { clip } from './classifier-input.js'
|
|
9
10
|
import { fingerprintContent, staleFileError } from './content-fingerprint.js'
|
|
10
11
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
11
12
|
import { fileWriteResult } from './file-write-result.js'
|
|
@@ -93,6 +94,11 @@ export const WriteFileTool = defineTool({
|
|
|
93
94
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
94
95
|
// call runs, rather than refused after. See `ToolDefinition.pathArgument`.
|
|
95
96
|
pathArgument: 'path',
|
|
97
|
+
// The classifier judges where a file is written and how much, not the body.
|
|
98
|
+
classifierInput: (input: WriteInput) => {
|
|
99
|
+
const body = input.content ?? input.newStr ?? ''
|
|
100
|
+
return `${input.path}: writes ${body.length} characters, starting ${JSON.stringify(clip(body, 300))}`
|
|
101
|
+
},
|
|
96
102
|
permissions: ['file_write'],
|
|
97
103
|
readOnly: false,
|
|
98
104
|
destructive: true,
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
33
33
|
capturesScreen?: boolean | ((input: z.infer<S>) => boolean)
|
|
34
34
|
/** Whether this exact call always needs a person's approval; see {@link ToolDefinition.requiresApproval}. */
|
|
35
35
|
requiresApproval?: boolean | ((input: z.infer<S>) => boolean)
|
|
36
|
+
/** What the auto-mode classifier is shown of this call; see {@link ToolDefinition.classifierInput}. */
|
|
37
|
+
classifierInput?: (input: z.infer<S>) => string
|
|
36
38
|
/** Free-form filtering data, never sent to the model; see {@link ToolDefinition.metadata}. */
|
|
37
39
|
metadata?: Readonly<Record<string, unknown>>
|
|
38
40
|
/** Batch ordering boundary; see {@link ToolDefinition.executionBarrier}. */
|
|
@@ -186,6 +188,7 @@ export function defineTool<S extends z.ZodType>(
|
|
|
186
188
|
: () => options.requiresApproval as boolean,
|
|
187
189
|
}
|
|
188
190
|
: {}),
|
|
191
|
+
...(options.classifierInput !== undefined ? { classifierInput: options.classifierInput } : {}),
|
|
189
192
|
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
190
193
|
|
|
191
194
|
async execute(input: TInput, context: ToolContext): Promise<ToolResult> {
|
package/src/types/acp/index.ts
CHANGED
|
@@ -43,6 +43,13 @@ export interface AcpInitializeResult {
|
|
|
43
43
|
readonly promptAttachments?: boolean
|
|
44
44
|
/** True only when this host applies captured effort and review settings per prompt. */
|
|
45
45
|
readonly promptOptions?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* The review modes beyond the original five (`prompt`, `plan`,
|
|
48
|
+
* `accept-edits`, `auto`, `strict`) that this host applies per prompt:
|
|
49
|
+
* `bypass` and `classify`, when it supports them. Absent: it accepts only
|
|
50
|
+
* the original five, and refuses the others before a turn starts.
|
|
51
|
+
*/
|
|
52
|
+
readonly promptPermissionModes?: readonly ('bypass' | 'classify')[]
|
|
46
53
|
/** Optional, explicitly installed Namzu host methods; core ACP peers can ignore them. */
|
|
47
54
|
readonly extensions?: readonly string[]
|
|
48
55
|
readonly protocolVersion: number
|
|
@@ -119,6 +119,14 @@ export interface AnalysisFunction {
|
|
|
119
119
|
readonly id: number
|
|
120
120
|
/** `null` for an anonymous function with nothing to infer a name from. */
|
|
121
121
|
readonly name: string | null
|
|
122
|
+
/**
|
|
123
|
+
* `name` qualified by what holds it, when it is a member: `Svc.init` for a
|
|
124
|
+
* method of the class `Svc` (a class expression assigned to a variable takes
|
|
125
|
+
* the variable's name), `obj.run` for a method of an object literal assigned
|
|
126
|
+
* to `obj`. Absent for a function that is not a member. `functionSource`
|
|
127
|
+
* accepts either spelling.
|
|
128
|
+
*/
|
|
129
|
+
readonly qualifiedName?: string
|
|
122
130
|
readonly kind: AnalysisFunctionKind
|
|
123
131
|
/** Absolute path. */
|
|
124
132
|
readonly path: string
|
|
@@ -136,6 +144,17 @@ export interface AnalysisSourceSlice {
|
|
|
136
144
|
readonly chars: number
|
|
137
145
|
/** `true` when `text` is shorter than `chars` because of the size cap. */
|
|
138
146
|
readonly truncated: boolean
|
|
147
|
+
/**
|
|
148
|
+
* Present when the cut was made around a position rather than at the end:
|
|
149
|
+
* `text` is the function's first `headerChars` characters, a marker for
|
|
150
|
+
* what was left out, then the file's own characters `from` to `to`, which
|
|
151
|
+
* contain the position asked about. `from` and `to` are offsets into the file.
|
|
152
|
+
*/
|
|
153
|
+
readonly window?: {
|
|
154
|
+
readonly headerChars: number
|
|
155
|
+
readonly from: number
|
|
156
|
+
readonly to: number
|
|
157
|
+
}
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
export interface IndexRequest extends AnalysisCallOptions {
|
|
@@ -222,6 +241,11 @@ export interface EnclosingFunctionRequest extends AnalysisCallOptions {
|
|
|
222
241
|
export interface AnalysisEnclosingFunction {
|
|
223
242
|
/** `null` when the position is not inside any function — a real answer. */
|
|
224
243
|
readonly function: AnalysisFunction | null
|
|
244
|
+
/**
|
|
245
|
+
* When `function` is `null` because the position is in a class body outside
|
|
246
|
+
* every method (a field initializer, a `static {}` block): that class.
|
|
247
|
+
*/
|
|
248
|
+
readonly inClass?: string
|
|
225
249
|
/** The function's parents, innermost first. */
|
|
226
250
|
readonly parents: readonly AnalysisFunction[]
|
|
227
251
|
readonly source?: AnalysisSourceSlice
|
|
@@ -235,6 +259,11 @@ export interface FunctionSourceRequest extends AnalysisPaging {
|
|
|
235
259
|
readonly path?: string
|
|
236
260
|
readonly name?: string
|
|
237
261
|
readonly maxChars?: number
|
|
262
|
+
/**
|
|
263
|
+
* A position in the function. When the source is longer than `maxChars`, the
|
|
264
|
+
* slice is cut around it instead of at the end, and always contains it.
|
|
265
|
+
*/
|
|
266
|
+
readonly around?: { readonly line: number; readonly column?: number }
|
|
238
267
|
}
|
|
239
268
|
|
|
240
269
|
export interface AnalysisFunctionSource {
|
|
@@ -245,7 +274,11 @@ export interface AnalysisFunctionSource {
|
|
|
245
274
|
|
|
246
275
|
export interface CallersOfRequest extends AnalysisPaging {
|
|
247
276
|
readonly root: string
|
|
248
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* The callee text: `foo`, `obj.foo`, `this.foo`. A method's qualified name
|
|
279
|
+
* (`Svc.init`, as `functionSource` shows it) finds `this.init()` inside
|
|
280
|
+
* `Svc` as a resolved call, and every other `<expr>.init()` as `text-only`.
|
|
281
|
+
*/
|
|
249
282
|
readonly name: string
|
|
250
283
|
/**
|
|
251
284
|
* `file` looks in `path` only and is the default, because a callee matched
|
|
@@ -266,8 +299,19 @@ export interface AnalysisCallSite {
|
|
|
266
299
|
readonly line: number
|
|
267
300
|
readonly column: number
|
|
268
301
|
readonly callee: string
|
|
269
|
-
|
|
302
|
+
/**
|
|
303
|
+
* `call-via` is `f.call(…)`, `f.apply(…)` or `f.bind(…)` of the callee (a
|
|
304
|
+
* use, not necessarily a call); `tagged` is a tagged template.
|
|
305
|
+
*/
|
|
306
|
+
readonly kind: 'call' | 'new' | 'call-via' | 'tagged'
|
|
270
307
|
readonly argumentCount: number
|
|
308
|
+
/**
|
|
309
|
+
* Only when the request named a method by its qualified name: `resolved`
|
|
310
|
+
* for `this.m()` inside the class that declares it and for the qualified
|
|
311
|
+
* text itself, `text-only` for any other receiver, which may be an
|
|
312
|
+
* unrelated method of the same name.
|
|
313
|
+
*/
|
|
314
|
+
readonly resolution?: 'resolved' | 'text-only'
|
|
271
315
|
/** The function the call sits in; absent at the top level of a file. */
|
|
272
316
|
readonly caller?: AnalysisFunction
|
|
273
317
|
/** See {@link AnalysisLiteralHit.stale}. */
|
|
@@ -290,7 +334,11 @@ export type AnalysisModuleEdge =
|
|
|
290
334
|
}
|
|
291
335
|
| {
|
|
292
336
|
readonly direction: 'export'
|
|
293
|
-
|
|
337
|
+
/**
|
|
338
|
+
* `type` is a type-only export, `commonjs` is `module.exports` or
|
|
339
|
+
* `exports.x`, `assign` is `export =`.
|
|
340
|
+
*/
|
|
341
|
+
readonly kind: 'named' | 'default' | 'reexport' | 'star' | 'type' | 'commonjs' | 'assign'
|
|
294
342
|
readonly name: string
|
|
295
343
|
readonly source?: string
|
|
296
344
|
readonly line: number
|
package/src/types/hitl/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CostInfo, TokenUsage } from '../common/index.js'
|
|
2
2
|
import type { CheckpointId, PlanId, SessionId, TurnId } from '../ids/index.js'
|
|
3
|
+
import type { Message } from '../message/index.js'
|
|
3
4
|
|
|
4
5
|
export type { CheckpointId }
|
|
5
6
|
|
|
@@ -139,7 +140,28 @@ export type HITLDecisionRequest =
|
|
|
139
140
|
question: UserQuestionData
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
/**
|
|
144
|
+
* What the turn offers a resume handler besides the request, in memory only.
|
|
145
|
+
*
|
|
146
|
+
* Kept out of {@link HITLDecisionRequest} on purpose: the request is written
|
|
147
|
+
* to the session log when a decision takes long, and a transcript does not
|
|
148
|
+
* belong there. A handler that does not need it ignores the argument; one that
|
|
149
|
+
* wraps another passes it on.
|
|
150
|
+
*/
|
|
151
|
+
export interface ResumeContext {
|
|
152
|
+
/**
|
|
153
|
+
* The conversation as it stands now, the batch under review included.
|
|
154
|
+
* Read on demand; the array is the turn's own, so a caller must not change it.
|
|
155
|
+
*/
|
|
156
|
+
readonly transcript: () => readonly Message[]
|
|
157
|
+
/** The turn's cancellation signal, when it has one. */
|
|
158
|
+
readonly signal?: AbortSignal
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type ResumeHandler = (
|
|
162
|
+
request: HITLDecisionRequest,
|
|
163
|
+
context?: ResumeContext,
|
|
164
|
+
) => Promise<HITLResumeDecision>
|
|
143
165
|
|
|
144
166
|
export interface ToolCallSummary {
|
|
145
167
|
id: string
|
|
@@ -158,6 +180,24 @@ export interface ToolCallSummary {
|
|
|
158
180
|
reason?: string
|
|
159
181
|
/** A matching rule requested review; read-only exemptions must not bypass it. */
|
|
160
182
|
explicitReview?: true
|
|
183
|
+
/**
|
|
184
|
+
* A rule written to ask a person (a `review` rule), or a hook that asked
|
|
185
|
+
* for one, requested this review. Unlike {@link explicitReview} it is not
|
|
186
|
+
* set when an `allow` rule was demoted to review because the call reaches
|
|
187
|
+
* past its boundary. `bypass` and `classify` keep this question for a
|
|
188
|
+
* person; they answer the demoted kind themselves.
|
|
189
|
+
*/
|
|
190
|
+
askRule?: true
|
|
191
|
+
/**
|
|
192
|
+
* How wide the `allow` rule that settled this call is. `tool` means the
|
|
193
|
+
* rule names the whole tool, a category, a tier or a source, or its
|
|
194
|
+
* pattern matches every input; `pattern` means it matches some inputs
|
|
195
|
+
* only. Present only when `decision` is `allow` and a rule (not the
|
|
196
|
+
* read-only allowance) decided it. The `classify` review mode reads it:
|
|
197
|
+
* a whole-tool allow for a shell tool would let the classifier never
|
|
198
|
+
* see a command.
|
|
199
|
+
*/
|
|
200
|
+
allowScope?: 'tool' | 'pattern'
|
|
161
201
|
}
|
|
162
202
|
/**
|
|
163
203
|
* What this call reaches beyond the turn's ordinary boundary, as the
|
|
@@ -278,6 +318,14 @@ export interface ToolCallEscalation {
|
|
|
278
318
|
* tool that declares `commandArgument`.
|
|
279
319
|
*/
|
|
280
320
|
dangerousRemoval?: string
|
|
321
|
+
/**
|
|
322
|
+
* Why the kernel could not finish looking at this call: the check that
|
|
323
|
+
* finds the escalations above threw, so none of them is known to be
|
|
324
|
+
* absent. Treated as the most serious escalation there is, because the
|
|
325
|
+
* safety checks did not run: a person is asked in every mode, `bypass`
|
|
326
|
+
* and `classify` included, and it is refused when nobody can be asked.
|
|
327
|
+
*/
|
|
328
|
+
unfinishedCheck?: string
|
|
281
329
|
}
|
|
282
330
|
|
|
283
331
|
export interface ToolModification {
|
package/src/types/tool/index.ts
CHANGED
|
@@ -1178,6 +1178,23 @@ export interface ToolDefinition<TInput = unknown> extends ToolPresentation<TInpu
|
|
|
1178
1178
|
*/
|
|
1179
1179
|
requiresApproval?(input: TInput): boolean
|
|
1180
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* What the auto-mode safety classifier is shown of this call.
|
|
1183
|
+
*
|
|
1184
|
+
* The classifier (`createAutoReviewClassifier`, review mode `classify`)
|
|
1185
|
+
* reads a compact transcript, not raw tool arguments: a tool knows which
|
|
1186
|
+
* part of its own input says what the call DOES. A shell tool returns the
|
|
1187
|
+
* command, a file tool the path and a short summary of the change, a fetch
|
|
1188
|
+
* the address. The string should be short and carry no content the model
|
|
1189
|
+
* could not already be trusted with; the runtime caps its length.
|
|
1190
|
+
*
|
|
1191
|
+
* Absent, the classifier is shown the input as compact JSON. A tool whose
|
|
1192
|
+
* input is large or opaque (a file body, a binary payload) should declare
|
|
1193
|
+
* this so the classifier judges the action rather than the bulk. Never
|
|
1194
|
+
* called with a tool RESULT: results are untrusted and are not shown.
|
|
1195
|
+
*/
|
|
1196
|
+
classifierInput?(input: TInput): string
|
|
1197
|
+
|
|
1181
1198
|
/**
|
|
1182
1199
|
* Free-form, tool-author-declared data for filtering and behaviour
|
|
1183
1200
|
* customization — never a classification the runtime itself reads.
|