@namzu/sdk 53.2.2 → 54.0.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 +24 -0
- package/dist/compaction/context-breakdown.d.ts +85 -0
- package/dist/compaction/context-breakdown.d.ts.map +1 -0
- package/dist/compaction/context-breakdown.js +149 -0
- package/dist/compaction/context-breakdown.js.map +1 -0
- package/dist/compaction/token-estimate.d.ts +2 -0
- package/dist/compaction/token-estimate.d.ts.map +1 -1
- package/dist/compaction/token-estimate.js +4 -0
- package/dist/compaction/token-estimate.js.map +1 -1
- package/dist/public-runtime.d.ts +7 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +6 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/cache-clock.d.ts +39 -0
- package/dist/runtime/cache-clock.d.ts.map +1 -0
- package/dist/runtime/cache-clock.js +48 -0
- package/dist/runtime/cache-clock.js.map +1 -0
- package/dist/runtime/query/compact-session.d.ts +37 -2
- package/dist/runtime/query/compact-session.d.ts.map +1 -1
- package/dist/runtime/query/compact-session.js +77 -3
- package/dist/runtime/query/compact-session.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 +3 -0
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +8 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/store/budget/disk.js +4 -1
- package/dist/store/budget/disk.js.map +1 -1
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +6 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +23 -9
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +7 -4
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +3 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +27 -7
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/exit-semantics.d.ts +31 -0
- package/dist/tools/builtins/exit-semantics.d.ts.map +1 -0
- package/dist/tools/builtins/exit-semantics.js +198 -0
- package/dist/tools/builtins/exit-semantics.js.map +1 -0
- package/dist/tools/builtins/write-file.js +7 -2
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/memory/delete.js +1 -1
- package/dist/tools/memory/delete.js.map +1 -1
- package/dist/tools/memory/read.js +1 -1
- package/dist/tools/memory/read.js.map +1 -1
- package/dist/tools/memory/save.d.ts.map +1 -1
- package/dist/tools/memory/save.js +4 -3
- package/dist/tools/memory/save.js.map +1 -1
- package/dist/tools/memory/search.js +1 -1
- package/dist/tools/memory/search.js.map +1 -1
- package/dist/tools/memory/update.js +1 -1
- package/dist/tools/memory/update.js.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +6 -1
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/common/index.d.ts +19 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +9 -0
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +8 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +6 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/session/checkpoint.d.ts +52 -0
- package/dist/types/session/checkpoint.d.ts.map +1 -1
- package/dist/types/session/checkpoint.js +5 -0
- package/dist/types/session/checkpoint.js.map +1 -1
- package/dist/types/session/events.d.ts +9 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +181 -6
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +17 -1
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-breakdown.ts +228 -0
- package/src/compaction/token-estimate.ts +5 -0
- package/src/public-runtime.ts +23 -2
- package/src/runtime/cache-clock.ts +67 -0
- package/src/runtime/query/compact-session.ts +101 -3
- package/src/runtime/query/executor/tool-call-admission.ts +3 -0
- package/src/runtime/query/iteration/index.ts +8 -0
- package/src/store/budget/disk.ts +4 -1
- package/src/tools/admit-facts.ts +6 -0
- package/src/tools/builtins/bash.ts +25 -9
- package/src/tools/builtins/edit-apply.ts +12 -2
- package/src/tools/builtins/edit.ts +34 -8
- package/src/tools/builtins/exit-semantics.ts +198 -0
- package/src/tools/builtins/write-file.ts +7 -2
- package/src/tools/memory/delete.ts +1 -1
- package/src/tools/memory/read.ts +1 -1
- package/src/tools/memory/save.ts +4 -3
- package/src/tools/memory/search.ts +1 -1
- package/src/tools/memory/update.ts +1 -1
- package/src/toolsets/manager.ts +6 -1
- package/src/types/common/index.ts +27 -0
- package/src/types/provider/chat.ts +8 -0
- package/src/types/provider/config.ts +6 -0
- package/src/types/session/checkpoint.ts +5 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +1 -1
- package/src/types/tool/presentation.ts +16 -0
|
@@ -23,6 +23,7 @@ import { defineTool } from '../defineTool.js'
|
|
|
23
23
|
import { scrubInheritedEnv } from '../env-scrub.js'
|
|
24
24
|
import { createHostOutputDecoder } from '../host-output.js'
|
|
25
25
|
import { looksBinary, windowText } from '../text-window.js'
|
|
26
|
+
import { judgeExit } from './exit-semantics.js'
|
|
26
27
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
27
28
|
|
|
28
29
|
// Namzu owns its own bash timeout knob — `NAMZU_BASH_TIMEOUT_MS` — and
|
|
@@ -669,11 +670,14 @@ const BashToolBase = defineTool({
|
|
|
669
670
|
.filter(Boolean)
|
|
670
671
|
.join('\n\n')
|
|
671
672
|
|
|
673
|
+
const judged = result.timedOut ? undefined : judgeExit(input.command, result.exitCode)
|
|
674
|
+
const answered = judged !== undefined && !judged.isError
|
|
672
675
|
return {
|
|
673
|
-
success:
|
|
676
|
+
success: answered,
|
|
674
677
|
output: output || '(no output)',
|
|
675
678
|
data: {
|
|
676
679
|
exitCode: result.exitCode,
|
|
680
|
+
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
677
681
|
sandboxed: true,
|
|
678
682
|
timedOut: result.timedOut,
|
|
679
683
|
stdoutTruncated: result.stdoutTruncated ?? false,
|
|
@@ -681,12 +685,13 @@ const BashToolBase = defineTool({
|
|
|
681
685
|
},
|
|
682
686
|
error: result.timedOut
|
|
683
687
|
? `Command timed out after ${input.timeout}ms. Any captured output before the deadline is above.`
|
|
684
|
-
:
|
|
688
|
+
: !answered
|
|
685
689
|
? `Command exited with code ${result.exitCode}`
|
|
686
690
|
: undefined,
|
|
687
691
|
facts: commandFacts({
|
|
688
692
|
command: input.command,
|
|
689
693
|
exitCode: result.exitCode,
|
|
694
|
+
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
690
695
|
timedOut: result.timedOut,
|
|
691
696
|
stdout: result.stdout,
|
|
692
697
|
stderr: result.stderr,
|
|
@@ -801,35 +806,44 @@ const BashToolBase = defineTool({
|
|
|
801
806
|
// and appending this to every result would make the common case noisy
|
|
802
807
|
// to buy nothing. A failing one is exactly where "authentication
|
|
803
808
|
// failed" has to be distinguishable from "the variable was withheld".
|
|
809
|
+
// A status the command uses to answer (no match, files differ) is a
|
|
810
|
+
// result, not a failure; the model sees it as an ordinary call.
|
|
811
|
+
const judged =
|
|
812
|
+
!timedOut && exitCode !== undefined ? judgeExit(input.command, exitCode) : undefined
|
|
813
|
+
const answered = judged !== undefined && !judged.isError
|
|
804
814
|
const output = [
|
|
805
815
|
formatShellOutput(failure.stdout, failure.stderr),
|
|
806
816
|
clipped.length > 0
|
|
807
817
|
? `[${clipped.join(' and ')} ${clipped.length === 1 ? 'was' : 'were'} truncated by the host output cap. The omitted output is unavailable in this result.]`
|
|
808
818
|
: '',
|
|
809
|
-
describeWithheldEnv(inherited.dropped),
|
|
819
|
+
answered ? '' : describeWithheldEnv(inherited.dropped),
|
|
810
820
|
]
|
|
811
821
|
.filter(Boolean)
|
|
812
822
|
.join('\n\n')
|
|
813
823
|
|
|
814
824
|
return {
|
|
815
|
-
success:
|
|
825
|
+
success: answered,
|
|
816
826
|
output: output || '(no output)',
|
|
817
827
|
data: {
|
|
818
828
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
829
|
+
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
819
830
|
...(leaveSandbox ? { sandboxed: false, sandboxEscape: true } : {}),
|
|
820
831
|
timedOut,
|
|
821
832
|
stdoutTruncated: failure.stdoutTruncated ?? false,
|
|
822
833
|
stderrTruncated: failure.stderrTruncated ?? false,
|
|
823
834
|
...(failure.signal ? { signal: failure.signal } : {}),
|
|
824
835
|
},
|
|
825
|
-
error:
|
|
826
|
-
?
|
|
827
|
-
:
|
|
828
|
-
? `Command
|
|
829
|
-
:
|
|
836
|
+
error: answered
|
|
837
|
+
? undefined
|
|
838
|
+
: timedOut
|
|
839
|
+
? `Command timed out after ${input.timeout}ms. Any output it produced before the deadline is above.`
|
|
840
|
+
: exitCode !== undefined
|
|
841
|
+
? `Command exited with code ${exitCode}`
|
|
842
|
+
: `Command failed: ${failure.message}`,
|
|
830
843
|
facts: commandFacts({
|
|
831
844
|
command: input.command,
|
|
832
845
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
846
|
+
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
833
847
|
timedOut,
|
|
834
848
|
...(failure.signal ? { signal: failure.signal } : {}),
|
|
835
849
|
stdout: failure.stdout ?? '',
|
|
@@ -851,6 +865,7 @@ const BashToolBase = defineTool({
|
|
|
851
865
|
function commandFacts(input: {
|
|
852
866
|
command: string
|
|
853
867
|
exitCode?: number
|
|
868
|
+
exitNote?: string
|
|
854
869
|
timedOut?: boolean
|
|
855
870
|
signal?: string
|
|
856
871
|
stdout: string
|
|
@@ -867,6 +882,7 @@ function commandFacts(input: {
|
|
|
867
882
|
kind: 'command',
|
|
868
883
|
command: input.command,
|
|
869
884
|
...(input.exitCode !== undefined ? { exitCode: input.exitCode } : {}),
|
|
885
|
+
...(input.exitNote ? { exitNote: input.exitNote } : {}),
|
|
870
886
|
...(input.timedOut ? { timedOut: true } : {}),
|
|
871
887
|
...(input.signal ? { signal: input.signal } : {}),
|
|
872
888
|
...(input.stdout && !outBinary ? { stdout: windowText(input.stdout, outBytes) } : {}),
|
|
@@ -130,6 +130,13 @@ function normalizeInsertLine(
|
|
|
130
130
|
return { success: true, value }
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** A refused edit, with the kind of refusal when it is one a host can phrase. */
|
|
134
|
+
export type EditApplyFailure = {
|
|
135
|
+
success: false
|
|
136
|
+
error: string
|
|
137
|
+
reason?: 'text_not_found' | 'not_unique'
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
/**
|
|
134
141
|
* Apply every operation in order, or none of them.
|
|
135
142
|
*
|
|
@@ -149,7 +156,7 @@ function normalizeInsertLine(
|
|
|
149
156
|
export function applyEdit(
|
|
150
157
|
content: string,
|
|
151
158
|
operations: readonly NormalizedEditInput[],
|
|
152
|
-
): { success: true; content: string; replacements: number } |
|
|
159
|
+
): { success: true; content: string; replacements: number } | EditApplyFailure {
|
|
153
160
|
let current = content
|
|
154
161
|
let replacements = 0
|
|
155
162
|
|
|
@@ -159,6 +166,7 @@ export function applyEdit(
|
|
|
159
166
|
return {
|
|
160
167
|
success: false,
|
|
161
168
|
error: framedError(result.error, index, operations.length),
|
|
169
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
current = result.content
|
|
@@ -171,7 +179,7 @@ export function applyEdit(
|
|
|
171
179
|
function applyOne(
|
|
172
180
|
content: string,
|
|
173
181
|
input: NormalizedEditInput,
|
|
174
|
-
): { success: true; content: string; replacements: number } |
|
|
182
|
+
): { success: true; content: string; replacements: number } | EditApplyFailure {
|
|
175
183
|
if (input.operation === 'insert') {
|
|
176
184
|
return applyLineInsert(content, input)
|
|
177
185
|
}
|
|
@@ -191,6 +199,7 @@ function applyOne(
|
|
|
191
199
|
if (resolved === null) {
|
|
192
200
|
return {
|
|
193
201
|
success: false,
|
|
202
|
+
reason: 'text_not_found',
|
|
194
203
|
error:
|
|
195
204
|
'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
|
|
196
205
|
}
|
|
@@ -225,6 +234,7 @@ function applyOne(
|
|
|
225
234
|
const secondLine = content.slice(0, secondIndex).split('\n').length
|
|
226
235
|
return {
|
|
227
236
|
success: false,
|
|
237
|
+
reason: 'not_unique',
|
|
228
238
|
error: `old_string/oldStr is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
|
|
229
239
|
}
|
|
230
240
|
}
|
|
@@ -2,12 +2,18 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import type { ToolContext, ToolResult } from '../../types/tool/index.js'
|
|
5
|
+
import type { ToolFacts } from '../../types/tool/presentation.js'
|
|
5
6
|
import { defineTool } from '../defineTool.js'
|
|
6
7
|
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
7
8
|
import { atomicWriteFile } from './atomic-write-file.js'
|
|
8
9
|
import { clip, describeChange } from './classifier-input.js'
|
|
9
10
|
import { fingerprintContent, staleFileError, unreadFileError } from './content-fingerprint.js'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type EditApplyFailure,
|
|
13
|
+
type NormalizedEditInput,
|
|
14
|
+
applyEdit,
|
|
15
|
+
normalizeEditInput,
|
|
16
|
+
} from './edit-apply.js'
|
|
11
17
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
12
18
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
13
19
|
import { lineDiff } from './line-diff.js'
|
|
@@ -334,7 +340,7 @@ function recordDrift(context: ToolContext, key: string): void {
|
|
|
334
340
|
}
|
|
335
341
|
|
|
336
342
|
type EditAdmission =
|
|
337
|
-
| { success: false; output: ''; error: string }
|
|
343
|
+
| { success: false; output: ''; error: string; facts: ToolFacts }
|
|
338
344
|
| { success: true; result: ReturnType<typeof applyEdit> }
|
|
339
345
|
|
|
340
346
|
/**
|
|
@@ -359,7 +365,12 @@ function admitAgainstRead(
|
|
|
359
365
|
): EditAdmission {
|
|
360
366
|
const tracker = context.fileReadTracker
|
|
361
367
|
if (tracker && !tracker.hasRead(key)) {
|
|
362
|
-
return {
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
output: '',
|
|
371
|
+
error: unreadFileError(key, 'edit'),
|
|
372
|
+
facts: { kind: 'failure', reason: 'not_read', subject: key },
|
|
373
|
+
}
|
|
363
374
|
}
|
|
364
375
|
const seen = tracker?.fingerprint?.(key)
|
|
365
376
|
const applied = applyEdit(content, operations)
|
|
@@ -367,7 +378,12 @@ function admitAgainstRead(
|
|
|
367
378
|
const recoverable = applied.success && operations.every((o) => o.operation === 'replace')
|
|
368
379
|
if (!recoverable) {
|
|
369
380
|
recordDrift(context, key)
|
|
370
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
output: '',
|
|
384
|
+
error: staleFileError(key),
|
|
385
|
+
facts: { kind: 'failure', reason: 'changed_on_disk', subject: key },
|
|
386
|
+
}
|
|
371
387
|
}
|
|
372
388
|
}
|
|
373
389
|
return { success: true, result: applied }
|
|
@@ -397,6 +413,18 @@ function editFacts(path: string, replacements: number, before: string, after: st
|
|
|
397
413
|
}
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
/** A refused edit, with the kind of refusal when the text said it. */
|
|
417
|
+
function refusedEdit(result: EditApplyFailure, path: string) {
|
|
418
|
+
return {
|
|
419
|
+
success: false as const,
|
|
420
|
+
output: '' as const,
|
|
421
|
+
error: result.error,
|
|
422
|
+
...(result.reason
|
|
423
|
+
? { facts: { kind: 'failure' as const, reason: result.reason, subject: path } }
|
|
424
|
+
: {}),
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
400
428
|
export const EditTool = defineTool({
|
|
401
429
|
name: 'edit',
|
|
402
430
|
description:
|
|
@@ -529,9 +557,7 @@ export const EditTool = defineTool({
|
|
|
529
557
|
const admitted = admitAgainstRead(context, parsed.data.path, content, normalized.operations)
|
|
530
558
|
if (!admitted.success) return admitted
|
|
531
559
|
const result = admitted.result
|
|
532
|
-
if (!result.success)
|
|
533
|
-
return { success: false as const, output: '', error: result.error }
|
|
534
|
-
}
|
|
560
|
+
if (!result.success) return refusedEdit(result, parsed.data.path)
|
|
535
561
|
|
|
536
562
|
await context.sandbox.writeFile(parsed.data.path, result.content)
|
|
537
563
|
recordEditedContent(context, parsed.data.path, result.content)
|
|
@@ -554,7 +580,7 @@ export const EditTool = defineTool({
|
|
|
554
580
|
const admitted = admitAgainstRead(context, hostPath, content, normalized.operations)
|
|
555
581
|
if (!admitted.success) return admitted
|
|
556
582
|
const result = admitted.result
|
|
557
|
-
if (!result.success) return
|
|
583
|
+
if (!result.success) return refusedEdit(result, hostPath)
|
|
558
584
|
|
|
559
585
|
// Temp file, fsync, rename — a reader sees the old body or the new
|
|
560
586
|
// one, never a half-written one. A plain `writeFile` that fails
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a shell command's exit code is judged.
|
|
3
|
+
*
|
|
4
|
+
* Some programs use a non-zero status to answer a question rather than to
|
|
5
|
+
* report a failure: `grep` exits 1 when nothing matched, `diff` exits 1 when
|
|
6
|
+
* the files differ, `test` exits 1 when the condition is false. Reading those
|
|
7
|
+
* as failures turns a normal answer into a red error and sends the model off
|
|
8
|
+
* to "fix" it. Every other command keeps the plain rule: non-zero is a failure.
|
|
9
|
+
*
|
|
10
|
+
* The command is judged by the last command on its line (the one whose status
|
|
11
|
+
* the shell reports), but only when nothing earlier in an `&&` chain could
|
|
12
|
+
* have supplied that status: after `cd missing && grep x f` a 1 may come from
|
|
13
|
+
* `cd`, so the line is judged by the plain rule.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** What an exit code means for one command line. */
|
|
17
|
+
export interface ExitJudgement {
|
|
18
|
+
/** The command failed. */
|
|
19
|
+
readonly isError: boolean
|
|
20
|
+
/** A short plain sentence for a non-zero status that is not a failure. */
|
|
21
|
+
readonly note?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Longest line the splitter reads; a longer one gets the plain rule. */
|
|
25
|
+
const MAX_COMMAND_CHARS = 10_000
|
|
26
|
+
|
|
27
|
+
const PLAIN_FAILURE: ExitJudgement = { isError: true }
|
|
28
|
+
|
|
29
|
+
interface Parsed {
|
|
30
|
+
/** Command texts in order, with the separator that preceded each. */
|
|
31
|
+
readonly segments: readonly string[]
|
|
32
|
+
readonly separators: readonly string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cut a line at unquoted `|`, `|&`, `||`, `&&`, `;`, `&` and newlines.
|
|
37
|
+
* Undefined when the line has a shape this reading does not follow (an open
|
|
38
|
+
* quote, a subshell or group, a here-document): the caller then keeps the
|
|
39
|
+
* plain rule, which is the safe reading.
|
|
40
|
+
*/
|
|
41
|
+
function split(command: string): Parsed | undefined {
|
|
42
|
+
const segments: string[] = []
|
|
43
|
+
const separators: string[] = []
|
|
44
|
+
let current = ''
|
|
45
|
+
let pending = ''
|
|
46
|
+
let quote: '"' | "'" | null = null
|
|
47
|
+
const push = (): void => {
|
|
48
|
+
segments.push(current)
|
|
49
|
+
separators.push(pending)
|
|
50
|
+
current = ''
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < command.length; i++) {
|
|
53
|
+
const ch = command.charAt(i)
|
|
54
|
+
const next = command.charAt(i + 1)
|
|
55
|
+
if (quote === "'") {
|
|
56
|
+
if (ch === "'") quote = null
|
|
57
|
+
current += ch
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (ch === '\\') {
|
|
61
|
+
current += ch + next
|
|
62
|
+
i += 1
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (ch === '`') {
|
|
66
|
+
const end = command.indexOf('`', i + 1)
|
|
67
|
+
if (end === -1) return undefined
|
|
68
|
+
current += command.slice(i, end + 1)
|
|
69
|
+
i = end
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (ch === '$' && (next === '(' || next === '{')) {
|
|
73
|
+
const close = next === '(' ? ')' : '}'
|
|
74
|
+
let depth = 0
|
|
75
|
+
let j = i + 1
|
|
76
|
+
for (; j < command.length; j++) {
|
|
77
|
+
const c = command.charAt(j)
|
|
78
|
+
if (c === next) depth += 1
|
|
79
|
+
else if (c === close && --depth === 0) break
|
|
80
|
+
}
|
|
81
|
+
if (j >= command.length) return undefined
|
|
82
|
+
current += command.slice(i, j + 1)
|
|
83
|
+
i = j
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (quote === '"') {
|
|
87
|
+
if (ch === '"') quote = null
|
|
88
|
+
current += ch
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if (ch === '"' || ch === "'") {
|
|
92
|
+
quote = ch
|
|
93
|
+
current += ch
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (ch === '(' || ch === ')') return undefined
|
|
97
|
+
if (ch === '<' && next === '<' && command.charAt(i + 2) !== '<') return undefined
|
|
98
|
+
if (ch === '>' && next === '&') {
|
|
99
|
+
// `2>&1` and `>&2` are redirects, not a background `&`.
|
|
100
|
+
current += ch + next
|
|
101
|
+
i += 1
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (ch === '&' && command.charAt(i - 1) === '>') {
|
|
105
|
+
current += ch
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (ch === '|' || ch === '&' || ch === ';' || ch === '\n') {
|
|
109
|
+
let sep = ch
|
|
110
|
+
if ((ch === '|' || ch === '&') && next === ch) {
|
|
111
|
+
sep = ch + ch
|
|
112
|
+
i += 1
|
|
113
|
+
} else if (ch === '|' && next === '&') {
|
|
114
|
+
sep = '|&'
|
|
115
|
+
i += 1
|
|
116
|
+
}
|
|
117
|
+
push()
|
|
118
|
+
pending = sep === '\n' ? ';' : sep
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
current += ch
|
|
122
|
+
}
|
|
123
|
+
if (quote !== null) return undefined
|
|
124
|
+
push()
|
|
125
|
+
// A trailing separator (`a;`) leaves an empty last segment: drop it.
|
|
126
|
+
const kept: string[] = []
|
|
127
|
+
const keptSeparators: string[] = []
|
|
128
|
+
segments.forEach((segment, index) => {
|
|
129
|
+
if (segment.trim().length === 0) return
|
|
130
|
+
kept.push(segment.trim())
|
|
131
|
+
keptSeparators.push(separators[index] ?? '')
|
|
132
|
+
})
|
|
133
|
+
return kept.length === 0 ? undefined : { segments: kept, separators: keptSeparators }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether an earlier command in an `&&` chain could be the one whose status
|
|
138
|
+
* the line ended with: the last pipeline is joined to what precedes it by `&&`.
|
|
139
|
+
*/
|
|
140
|
+
function followsAndChain(parsed: Parsed): boolean {
|
|
141
|
+
let index = parsed.segments.length - 1
|
|
142
|
+
// Walk back over the pipeline the last command belongs to.
|
|
143
|
+
while (index > 0 && (parsed.separators[index] === '|' || parsed.separators[index] === '|&')) {
|
|
144
|
+
index -= 1
|
|
145
|
+
}
|
|
146
|
+
return parsed.separators[index] === '&&'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const NO_MATCHES = 'No matches found'
|
|
150
|
+
|
|
151
|
+
const SEARCH_WORDS = new Set(['grep', 'rg', 'egrep', 'fgrep'])
|
|
152
|
+
|
|
153
|
+
/** The subcommand of a `git` line, skipping global options (`-C <dir>`, `-c <k=v>`). */
|
|
154
|
+
function gitSubcommand(words: readonly string[]): string | undefined {
|
|
155
|
+
if (words[0] !== 'git') return undefined
|
|
156
|
+
for (let i = 1; i < words.length; i++) {
|
|
157
|
+
const word = words[i] as string
|
|
158
|
+
if (word.startsWith('-')) {
|
|
159
|
+
if (word === '-C' || word === '-c') i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
return word
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function questionNote(command: string): string | undefined {
|
|
168
|
+
const words = command.trim().split(/\s+/)
|
|
169
|
+
const first = words[0] ?? ''
|
|
170
|
+
if (first === 'git') {
|
|
171
|
+
const sub = gitSubcommand(words)
|
|
172
|
+
if (sub === 'diff') return 'Files differ'
|
|
173
|
+
if (sub === 'grep') return NO_MATCHES
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
if (SEARCH_WORDS.has(first)) return NO_MATCHES
|
|
177
|
+
if (first === 'find') return 'Some directories were inaccessible'
|
|
178
|
+
if (first === 'diff') return 'Files differ'
|
|
179
|
+
if (first === 'test' || first === '[') return 'Condition is false'
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Judge a finished command's exit code.
|
|
185
|
+
*
|
|
186
|
+
* Exit 0 is never a failure. For `grep`, `rg`, `egrep`, `fgrep`, `find`,
|
|
187
|
+
* `diff`, `test`, `[`, `git diff` and `git grep` as the last command of the
|
|
188
|
+
* line, exit 1 is an answer (with a short note) and 2 or more is a failure.
|
|
189
|
+
* Anything else, or a line the reading cannot follow, fails on any non-zero.
|
|
190
|
+
*/
|
|
191
|
+
export function judgeExit(command: string, exitCode: number): ExitJudgement {
|
|
192
|
+
if (exitCode === 0) return { isError: false }
|
|
193
|
+
if (exitCode !== 1 || command.length > MAX_COMMAND_CHARS) return PLAIN_FAILURE
|
|
194
|
+
const parsed = split(command)
|
|
195
|
+
if (!parsed || followsAndChain(parsed)) return PLAIN_FAILURE
|
|
196
|
+
const note = questionNote(parsed.segments[parsed.segments.length - 1] as string)
|
|
197
|
+
return note === undefined ? PLAIN_FAILURE : { isError: false, note }
|
|
198
|
+
}
|
|
@@ -210,7 +210,7 @@ function enforceReadBeforeOverwrite(
|
|
|
210
210
|
success: false,
|
|
211
211
|
output: '',
|
|
212
212
|
error: `${key} already exists. Use the \`read\` tool on it first in this conversation, then call \`write\` again — or prefer \`edit\` for a targeted change.`,
|
|
213
|
-
facts: { kind: 'failure', reason: '
|
|
213
|
+
facts: { kind: 'failure', reason: 'not_read', subject: key },
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -232,7 +232,12 @@ function enforceFreshOverwrite(
|
|
|
232
232
|
// so a reader that cannot touch the filesystem stops referencing this
|
|
233
233
|
// path. Optional on the interface; an older tracker keeps its behavior.
|
|
234
234
|
context.fileReadTracker?.recordDriftObserved?.(key)
|
|
235
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
output: '',
|
|
238
|
+
error: staleFileError(key, 'write'),
|
|
239
|
+
facts: { kind: 'failure', reason: 'changed_on_disk', subject: key },
|
|
240
|
+
}
|
|
236
241
|
}
|
|
237
242
|
return null
|
|
238
243
|
}
|
|
@@ -10,7 +10,7 @@ export function buildDeleteMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
10
10
|
return defineTool({
|
|
11
11
|
name: 'delete_memory',
|
|
12
12
|
description:
|
|
13
|
-
'Permanently
|
|
13
|
+
'Permanently remove a memory by ID, for one that should not be kept at all. Use update_memory with status archived when the record should stay available to inspect. Past conversation transcripts are not changed. Pass the revision from read_memory when available, so a deletion of a memory that changed since you read it is refused. Returns a confirmation.',
|
|
14
14
|
inputSchema: z.object({
|
|
15
15
|
id: z.string().describe('Memory ID to permanently delete'),
|
|
16
16
|
revision: z
|
package/src/tools/memory/read.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
16
16
|
return defineTool({
|
|
17
17
|
name: 'read_memory',
|
|
18
18
|
description:
|
|
19
|
-
'Read
|
|
19
|
+
'Read one memory in full by its ID or its name. Use it when the index or a search shows that a memory bears on the task. Returns the memory text; a text or Markdown memory ends with how old it is and the memories its [[name]] links point to.',
|
|
20
20
|
inputSchema: z.object({
|
|
21
21
|
id: z
|
|
22
22
|
.string()
|
package/src/tools/memory/save.ts
CHANGED
|
@@ -10,11 +10,12 @@ export function buildSaveMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
10
10
|
return defineTool({
|
|
11
11
|
name: 'save_memory',
|
|
12
12
|
description: [
|
|
13
|
-
'Save
|
|
14
|
-
'Choose a type: user (who the
|
|
13
|
+
'Save one memory for later sessions in this project: a single fact that the code, the history and the docs do not already hold.',
|
|
14
|
+
'Choose a type: user (who the person is and how they like to work), feedback (a rule they gave about how to work), project (a fact or decision about the work), reference (where to look for something).',
|
|
15
15
|
'For feedback and project memories, write the rule or fact first, then a line starting "Why:" and a line starting "How to apply:".',
|
|
16
16
|
'Link a related memory by writing [[its-name]] in the content.',
|
|
17
|
-
'
|
|
17
|
+
'The name must be new: a name another memory holds is refused, and the reply points to update_memory.',
|
|
18
|
+
"Returns the saved memory's id and name.",
|
|
18
19
|
].join(' '),
|
|
19
20
|
inputSchema: z.object({
|
|
20
21
|
title: z.string().min(1).describe('Short descriptive title'),
|
|
@@ -16,7 +16,7 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
|
|
|
16
16
|
return defineTool({
|
|
17
17
|
name: 'search_memory',
|
|
18
18
|
description:
|
|
19
|
-
|
|
19
|
+
"Find stored memories by words or tags. Use it when the topic is not in the index, before saving (to see whether a memory already covers it), and to reach what earlier turns recorded on their own. Matches are ranked across names, titles, descriptions, summaries and content; one call examines at most 256 recent candidates, and an incomplete result returns a scan_offset to continue from. Set status to archived to look at retired memories. Returns a numbered list with each memory's id, title, type and description; open one with read_memory.",
|
|
20
20
|
inputSchema: z.object({
|
|
21
21
|
query: z.string().optional().describe('Relevant words or identifiers to search'),
|
|
22
22
|
tags: z.array(z.string()).optional().describe('Filter by tags (all must match)'),
|
|
@@ -12,7 +12,7 @@ export function buildUpdateMemoryTool(store: MemoryStore): ToolDefinition {
|
|
|
12
12
|
return defineTool({
|
|
13
13
|
name: 'update_memory',
|
|
14
14
|
description:
|
|
15
|
-
'Correct
|
|
15
|
+
'Correct a memory in place, or archive one that is no longer true so it stops being recalled. Use it when a memory on the same topic already exists, which keeps the store free of duplicates, and when current evidence shows a memory is wrong. Identify the memory by the ID from search_memory or by its name from the index. Pass the revision from read_memory when available, so a change made since you read it is refused instead of overwritten. Returns the memory as it now stands.',
|
|
16
16
|
inputSchema: z.object({
|
|
17
17
|
id: z
|
|
18
18
|
.string()
|
package/src/toolsets/manager.ts
CHANGED
|
@@ -1012,7 +1012,12 @@ Executable tool names, descriptions, and JSON input schemas are attached through
|
|
|
1012
1012
|
'namzu.registry.errors': errorMessage,
|
|
1013
1013
|
'namzu.registry.empty': isEmptyInput,
|
|
1014
1014
|
})
|
|
1015
|
-
return {
|
|
1015
|
+
return {
|
|
1016
|
+
success: false,
|
|
1017
|
+
output: '',
|
|
1018
|
+
error: enrichedMessage,
|
|
1019
|
+
facts: { kind: 'failure', reason: 'invalid_arguments' },
|
|
1020
|
+
}
|
|
1016
1021
|
}
|
|
1017
1022
|
}
|
|
1018
1023
|
|
|
@@ -24,6 +24,24 @@ export interface TokenUsage {
|
|
|
24
24
|
* not zero.
|
|
25
25
|
*/
|
|
26
26
|
reasoningTokens?: number
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How long the provider keeps the prompt cache this call used, in
|
|
30
|
+
* milliseconds, as the call itself reported or requested it.
|
|
31
|
+
*
|
|
32
|
+
* A fact about ONE model call, so it is never summed: {@link accumulateTokenUsage}
|
|
33
|
+
* drops it, and {@link mergeTokenUsage} (frames of one streamed call) keeps the
|
|
34
|
+
* latest value. Absent means the driver does not report a lifetime (or does
|
|
35
|
+
* not cache); a host must not guess one.
|
|
36
|
+
*/
|
|
37
|
+
cacheTtlMs?: number
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cache-write tokens split by the lifetime they were written with. Present
|
|
41
|
+
* only when the provider breaks its write count down. Per call, like
|
|
42
|
+
* {@link TokenUsage.cacheTtlMs}.
|
|
43
|
+
*/
|
|
44
|
+
cacheWriteTtlTokens?: { fiveMin?: number; oneHour?: number }
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
export function accumulateTokenUsage(current: TokenUsage, addition: TokenUsage): TokenUsage {
|
|
@@ -40,6 +58,8 @@ export function accumulateTokenUsage(current: TokenUsage, addition: TokenUsage):
|
|
|
40
58
|
...(current.reasoningTokens !== undefined || addition.reasoningTokens !== undefined
|
|
41
59
|
? { reasoningTokens: (current.reasoningTokens ?? 0) + (addition.reasoningTokens ?? 0) }
|
|
42
60
|
: {}),
|
|
61
|
+
// `cacheTtlMs` and `cacheWriteTtlTokens` are per-call facts and are
|
|
62
|
+
// deliberately not carried into a sum.
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
|
|
@@ -74,6 +94,13 @@ export function mergeTokenUsage(current: TokenUsage, next: TokenUsage): TokenUsa
|
|
|
74
94
|
...(current.reasoningTokens !== undefined || next.reasoningTokens !== undefined
|
|
75
95
|
? { reasoningTokens: Math.max(current.reasoningTokens ?? 0, next.reasoningTokens ?? 0) }
|
|
76
96
|
: {}),
|
|
97
|
+
// Frames of one call describe the same cache: the latest report wins.
|
|
98
|
+
...(next.cacheTtlMs !== undefined || current.cacheTtlMs !== undefined
|
|
99
|
+
? { cacheTtlMs: next.cacheTtlMs ?? current.cacheTtlMs }
|
|
100
|
+
: {}),
|
|
101
|
+
...(next.cacheWriteTtlTokens !== undefined || current.cacheWriteTtlTokens !== undefined
|
|
102
|
+
? { cacheWriteTtlTokens: next.cacheWriteTtlTokens ?? current.cacheWriteTtlTokens }
|
|
103
|
+
: {}),
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
|
|
@@ -21,6 +21,14 @@ export type ResponseFormat =
|
|
|
21
21
|
|
|
22
22
|
export interface CacheControl {
|
|
23
23
|
type: 'auto' | 'ephemeral'
|
|
24
|
+
/**
|
|
25
|
+
* How long the provider should keep this prefix cached: `'5m'` (the
|
|
26
|
+
* default every provider uses when this is absent) or `'1h'`. Honoured only
|
|
27
|
+
* by a driver that has an extended lifetime; others ignore it. The longer
|
|
28
|
+
* lifetime is priced higher for the cache write, so asking for it is a cost
|
|
29
|
+
* decision, not a translation.
|
|
30
|
+
*/
|
|
31
|
+
ttl?: '5m' | '1h'
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export interface ChatCompletionParams {
|
|
@@ -157,6 +157,12 @@ export interface ProviderCapabilities {
|
|
|
157
157
|
/** Whether the driver maps document blocks returned by tools onto its result wire. */
|
|
158
158
|
supportsToolResultDocuments?: boolean
|
|
159
159
|
maxOutputTokens?: number
|
|
160
|
+
/**
|
|
161
|
+
* Prompt-cache lifetimes the driver can be asked for through
|
|
162
|
+
* `cacheControl.ttl`. Absent means the driver offers none (it has no cache,
|
|
163
|
+
* or the lifetime is the provider's own and cannot be chosen or read).
|
|
164
|
+
*/
|
|
165
|
+
cacheTtl?: readonly ('5m' | '1h')[]
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
export interface ProviderFactoryResult {
|
|
@@ -79,6 +79,11 @@ export const TokenUsageSchema = z
|
|
|
79
79
|
cachedTokens: count,
|
|
80
80
|
cacheWriteTokens: count,
|
|
81
81
|
reasoningTokens: count.optional(),
|
|
82
|
+
cacheTtlMs: count.optional(),
|
|
83
|
+
cacheWriteTtlTokens: z
|
|
84
|
+
.object({ fiveMin: count.optional(), oneHour: count.optional() })
|
|
85
|
+
.strict()
|
|
86
|
+
.optional(),
|
|
82
87
|
})
|
|
83
88
|
.strict()
|
|
84
89
|
|
|
@@ -811,6 +811,12 @@ type CoreSessionEvent =
|
|
|
811
811
|
turnId: TurnId
|
|
812
812
|
usage: TokenUsage
|
|
813
813
|
cost: CostInfo
|
|
814
|
+
/**
|
|
815
|
+
* The prompt cache after the latest model call alone (not the turn
|
|
816
|
+
* total above). Present only when the driver reports a lifetime;
|
|
817
|
+
* absent means unknown, and a host must not assume one.
|
|
818
|
+
*/
|
|
819
|
+
cache?: { ttlMs: number; touched: boolean }
|
|
814
820
|
/**
|
|
815
821
|
* How large the CONTEXT is right now, and how large it may get.
|
|
816
822
|
*
|
|
@@ -494,7 +494,7 @@ export const DecisionExpiredRecordSchema = inTurn('decision_expired', {
|
|
|
494
494
|
export const CompactionRecordSchema = recordSchema('compaction', {
|
|
495
495
|
compactionId: text.min(1),
|
|
496
496
|
strategy: text.min(1),
|
|
497
|
-
trigger: z.enum(['auto', 'manual']),
|
|
497
|
+
trigger: z.enum(['auto', 'manual', 'idle']),
|
|
498
498
|
replacesSeqRange: z.tuple([positive, positive]),
|
|
499
499
|
summary: z.union([z.array(messageBody), spill]),
|
|
500
500
|
keptMessageIds: z.array(messageId),
|