@namzu/sdk 53.2.1 → 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 +30 -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/browser.js +1 -1
- package/dist/tools/builtins/browser.js.map +1 -1
- package/dist/tools/builtins/computer-use.js +2 -2
- package/dist/tools/builtins/computer-use.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/coordinator/ask-user-question.js +1 -1
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/index.js +2 -2
- package/dist/tools/coordinator/index.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/fields.js +1 -1
- package/dist/tools/memory/fields.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/tools/schedules/schedule-tool.js +3 -3
- package/dist/tools/schedules/schedule-tool.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/browser.ts +1 -1
- package/src/tools/builtins/computer-use.ts +2 -2
- 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/coordinator/ask-user-question.ts +1 -1
- package/src/tools/coordinator/index.ts +2 -2
- package/src/tools/memory/delete.ts +1 -1
- package/src/tools/memory/fields.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/tools/schedules/schedule-tool.ts +3 -3
- 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) } : {}),
|
|
@@ -280,7 +280,7 @@ function actModelSchema(actions: readonly BrowserActActionName[]) {
|
|
|
280
280
|
origin: {
|
|
281
281
|
type: 'string',
|
|
282
282
|
description:
|
|
283
|
-
'REQUIRED. The origin from the latest snapshot header ("Page: <origin> — …"), e.g. https://
|
|
283
|
+
'REQUIRED. The origin from the latest snapshot header ("Page: <origin> — …"), e.g. https://example.com. The call is refused if the page is no longer there.',
|
|
284
284
|
},
|
|
285
285
|
ref: { type: 'string', description: 'Element ref from the latest snapshot, like e12.' },
|
|
286
286
|
text: { type: 'string' },
|
|
@@ -109,7 +109,7 @@ export function computerUseUnavailableReason(
|
|
|
109
109
|
model?: string,
|
|
110
110
|
): string | undefined {
|
|
111
111
|
if (!resolveProviderCapabilities(provider).supportsToolResultImages) {
|
|
112
|
-
return `The ${provider.id} provider cannot return images in tool results, so the model would never see a screenshot. Use a provider that can
|
|
112
|
+
return `The ${provider.id} provider cannot return images in tool results, so the model would never see a screenshot. Use a provider that can return images in tool results for computer use.`
|
|
113
113
|
}
|
|
114
114
|
// The driver can carry the image; the model behind it may still not read one.
|
|
115
115
|
// It would act on a screen it never saw, and the first screenshot would make
|
|
@@ -508,7 +508,7 @@ function buildDescription(
|
|
|
508
508
|
available.includes('type_text')
|
|
509
509
|
)
|
|
510
510
|
lines.push(
|
|
511
|
-
'To start a Windows program, a shell command (Start-Process
|
|
511
|
+
'To start a Windows program, a shell command (Start-Process <program>, or cmd.exe /c start <program>) is surest when you have a shell tool. The Start menu searches by display names in the system language, and ENTER on a name it does not find opens a web search in the browser.',
|
|
512
512
|
)
|
|
513
513
|
if (available.includes('list_windows'))
|
|
514
514
|
lines.push(
|
|
@@ -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
|
}
|
|
@@ -366,7 +366,7 @@ export function buildAskUserQuestionTool(config: AskUserQuestionToolOptions): To
|
|
|
366
366
|
return defineTool({
|
|
367
367
|
name: ASK_USER_QUESTION_TOOL_NAME,
|
|
368
368
|
description:
|
|
369
|
-
'Ask the user when you are blocked on a decision that is genuinely theirs to make — one you cannot resolve from their request, your tools, the files you can read, or sensible defaults. The question must be the genuinely undecidable thing in THIS task. Never ask for information a tool can discover (do not ask what you can read, list, or search), never re-ask what the conversation already answers, and never ask meta-questions like "Shall I proceed?" — plan ratification goes through approve_plan. Provide 2-4 genuinely distinct options derived from the actual context — concrete paths, never generic placeholders (for example, asked to prepare a presentation, ask
|
|
369
|
+
'Ask the user when you are blocked on a decision that is genuinely theirs to make — one you cannot resolve from their request, your tools, the files you can read, or sensible defaults. The question must be the genuinely undecidable thing in THIS task. Never ask for information a tool can discover (do not ask what you can read, list, or search), never re-ask what the conversation already answers, and never ask meta-questions like "Shall I proceed?" — plan ratification goes through approve_plan. Provide 2-4 genuinely distinct options derived from the actual context — concrete paths, never generic placeholders (for example, asked to prepare a presentation, ask who the audience is, with an option for each plausible audience); keep labels short (1-5 words) and give each option a one-line description of what practically changes if it is chosen. If you recommend an option, put it FIRST and set recommended: true on it; the host marks it for the user, so never write "(Recommended)" or any translation of it into a label, and never end a label with a parenthesised note (put qualifiers in the description). Set multiSelect: true only when several options can apply at once. An option may carry a preview (plain text or markdown) that the host shows beside the list while it is focused; use it only when seeing the thing helps the choice. A free-text "Something else" escape hatch is always shown automatically — do not add your own "Other" option. Put the questions in the questions array. Use ONE entry for a single decision. Put several entries in the same call only when they are related decisions that ALL block your next step and the user can answer them together (for example which account, for what purpose, which environment before a credentialed step), at most 4, each with a short unique header; the user answers them all in one card and you get one answer per question. Never batch questions that depend on one another\'s answers — ask the first, then decide. Most work needs at most 2-3 questions in all, so prefer proceeding on stated defaults over interrogating the user. Never invent answers or synthetic content on the user\'s behalf unless they explicitly asked for a random/test scenario. The answers arrive as this tool\'s result; a question the result says was not answered must not be assumed, and if the result says the user did not answer at all, do not ask again — proceed on your best judgment without assuming consent.',
|
|
370
370
|
inputSchema,
|
|
371
371
|
modelInputSchema: structuredClone(askUserQuestionModelInputSchema),
|
|
372
372
|
enforceModelInput: true,
|
|
@@ -252,7 +252,7 @@ const approvePlanModelInputSchema: Record<string, unknown> = {
|
|
|
252
252
|
properties: {
|
|
253
253
|
title: {
|
|
254
254
|
type: 'string',
|
|
255
|
-
description: 'Short title for the plan (e.g. "
|
|
255
|
+
description: 'Short title for the plan (e.g. "Security & Performance Review").',
|
|
256
256
|
},
|
|
257
257
|
summary: {
|
|
258
258
|
type: 'string',
|
|
@@ -1279,7 +1279,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
1279
1279
|
inputSchema: z.object({
|
|
1280
1280
|
title: z
|
|
1281
1281
|
.string()
|
|
1282
|
-
.describe('Short title for the plan (e.g. "
|
|
1282
|
+
.describe('Short title for the plan (e.g. "Security & Performance Review")'),
|
|
1283
1283
|
summary: z.string().describe('1-3 sentence summary of what you plan to do'),
|
|
1284
1284
|
steps: z
|
|
1285
1285
|
.preprocess(normalizeApprovePlanSteps, z.array(approvePlanStepSchema))
|
|
@@ -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
|
|
@@ -9,7 +9,7 @@ export const memoryFieldsSchema = {
|
|
|
9
9
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
|
10
10
|
.optional()
|
|
11
11
|
.describe(
|
|
12
|
-
'Unique kebab-case slug, e.g. "
|
|
12
|
+
'Unique kebab-case slug, e.g. "prefers-short-answers"; other memories link to it as [[name]]',
|
|
13
13
|
),
|
|
14
14
|
description: z
|
|
15
15
|
.string()
|
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()
|
|
@@ -121,7 +121,7 @@ const inputSchema = z.object({
|
|
|
121
121
|
rules: z
|
|
122
122
|
.record(z.string(), z.union([EFFECT, z.record(z.string(), EFFECT)]))
|
|
123
123
|
.optional()
|
|
124
|
-
.describe('Extra rules, e.g. {"bash": {"
|
|
124
|
+
.describe('Extra rules, e.g. {"bash": {"<command> *": "allow"}}'),
|
|
125
125
|
browser: z
|
|
126
126
|
.object({
|
|
127
127
|
profile: z
|
|
@@ -131,7 +131,7 @@ const inputSchema = z.object({
|
|
|
131
131
|
sites: z
|
|
132
132
|
.record(z.string(), z.enum(['read', 'ask', 'act']))
|
|
133
133
|
.describe(
|
|
134
|
-
'Site to level, e.g. {"https://
|
|
134
|
+
'Site to level, e.g. {"https://example.com": "read"}. read: open and read only; ask: changes wait for the operator; act: changes run unasked. Unlisted sites are denied; there is no "*".',
|
|
135
135
|
),
|
|
136
136
|
headed: z.boolean().optional().describe('Show the browser window; default no window'),
|
|
137
137
|
})
|
|
@@ -239,7 +239,7 @@ function browserGrant(
|
|
|
239
239
|
return {
|
|
240
240
|
ok: false,
|
|
241
241
|
error:
|
|
242
|
-
'permissions.browser.sites is empty; list each site the run may open, e.g. {"https://
|
|
242
|
+
'permissions.browser.sites is empty; list each site the run may open, e.g. {"https://example.com": "read"}.',
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
const sites: Record<string, ScheduleBrowserSiteLevel> = {}
|
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
|
|