@namzu/sdk 52.6.3 → 52.7.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 +13 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +1 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +9 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/session/api.d.ts +1 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +12 -2
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +10 -0
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +42 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +27 -12
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/file-write-result.d.ts.map +1 -1
- package/dist/tools/builtins/file-write-result.js +24 -31
- package/dist/tools/builtins/file-write-result.js.map +1 -1
- package/dist/tools/builtins/line-diff.d.ts +23 -0
- package/dist/tools/builtins/line-diff.d.ts.map +1 -0
- package/dist/tools/builtins/line-diff.js +112 -0
- package/dist/tools/builtins/line-diff.js.map +1 -0
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +1 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/session/events.d.ts +27 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +2 -2
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +3 -2
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +1 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/presentation.d.ts +22 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/bridge/a2a/mapper.ts +1 -0
- package/src/bridge/sse/mapper.ts +9 -0
- package/src/contracts/session/api.ts +1 -0
- package/src/execution/workflow/prelude.ts +12 -2
- package/src/runtime/query/iteration/phases/compaction.ts +11 -0
- package/src/tools/admit-facts.ts +46 -1
- package/src/tools/builtins/edit.ts +28 -12
- package/src/tools/builtins/file-write-result.ts +25 -34
- package/src/tools/builtins/line-diff.ts +122 -0
- package/src/turn/reporter.ts +1 -0
- package/src/types/session/events.ts +33 -2
- package/src/types/session/records.ts +3 -2
- package/src/types/tool/index.ts +1 -0
- package/src/types/tool/presentation.ts +23 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import type { ToolResult } from '../../types/tool/index.js'
|
|
2
|
+
import type { ToolDiffHunk, ToolResult } from '../../types/tool/index.js'
|
|
3
|
+
import { lineDiff } from './line-diff.js'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/** One side of a change, context included: the rows that exist before (`-`) or after (`+`). */
|
|
6
|
+
function previewSide(hunks: readonly ToolDiffHunk[], changed: '+' | '-'): string {
|
|
7
|
+
return hunks
|
|
8
|
+
.flatMap((hunk) => hunk.lines)
|
|
9
|
+
.filter((row) => row.startsWith(' ') || row.startsWith(changed))
|
|
10
|
+
.map((row) => `${row.slice(1)}\n`)
|
|
11
|
+
.join('')
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/** null means the backend could not establish the previous state. */
|
|
@@ -24,26 +26,10 @@ export function fileWriteResult(
|
|
|
24
26
|
: before === after
|
|
25
27
|
? 'unchanged'
|
|
26
28
|
: 'replace'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (typeof before === 'string') {
|
|
32
|
-
while (
|
|
33
|
-
start < oldLines.length &&
|
|
34
|
-
start < newLines.length &&
|
|
35
|
-
oldLines[start] === newLines[start]
|
|
36
|
-
)
|
|
37
|
-
start++
|
|
38
|
-
while (
|
|
39
|
-
end < oldLines.length - start &&
|
|
40
|
-
end < newLines.length - start &&
|
|
41
|
-
oldLines[oldLines.length - end - 1] === newLines[newLines.length - end - 1]
|
|
42
|
-
)
|
|
43
|
-
end++
|
|
44
|
-
}
|
|
45
|
-
const removed = oldLines.slice(start, oldLines.length - end)
|
|
46
|
-
const added = newLines.slice(start, newLines.length - end)
|
|
29
|
+
// Counted from a real line diff of the whole file, not from the span
|
|
30
|
+
// between the first and last changed line.
|
|
31
|
+
const diff = lineDiff(typeof before === 'string' ? before : '', after)
|
|
32
|
+
const known = before !== null
|
|
47
33
|
const bytes = Buffer.byteLength(after, 'utf8')
|
|
48
34
|
const label =
|
|
49
35
|
operation === 'create'
|
|
@@ -55,7 +41,7 @@ export function fileWriteResult(
|
|
|
55
41
|
: 'Wrote'
|
|
56
42
|
const newlineChanged =
|
|
57
43
|
typeof before === 'string' && before.endsWith('\n') !== after.endsWith('\n')
|
|
58
|
-
const summary = `${label} ${path}${before === null ? '' : ` (+${added
|
|
44
|
+
const summary = `${label} ${path}${before === null ? '' : ` (+${diff.added} -${diff.removed})`}${newlineChanged ? ' · final newline changed' : ''}`
|
|
59
45
|
return {
|
|
60
46
|
success: true,
|
|
61
47
|
output: `${summary} · ${bytes} bytes`,
|
|
@@ -71,7 +57,14 @@ export function fileWriteResult(
|
|
|
71
57
|
? 'unchanged'
|
|
72
58
|
: 'wrote',
|
|
73
59
|
bytes,
|
|
74
|
-
...(
|
|
60
|
+
...(known
|
|
61
|
+
? {
|
|
62
|
+
added: diff.added,
|
|
63
|
+
removed: diff.removed,
|
|
64
|
+
...(diff.hunks.length > 0 ? { hunks: diff.hunks } : {}),
|
|
65
|
+
...(diff.truncated ? { hunksTruncated: true as const } : {}),
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
75
68
|
},
|
|
76
69
|
data: {
|
|
77
70
|
path,
|
|
@@ -83,15 +76,13 @@ export function fileWriteResult(
|
|
|
83
76
|
? {}
|
|
84
77
|
: {
|
|
85
78
|
preview: {
|
|
86
|
-
before:
|
|
87
|
-
after:
|
|
79
|
+
before: previewSide(diff.hunks, '-'),
|
|
80
|
+
after: previewSide(diff.hunks, '+'),
|
|
88
81
|
},
|
|
89
82
|
}),
|
|
90
83
|
bytes,
|
|
91
84
|
sha256: createHash('sha256').update(after, 'utf8').digest('hex'),
|
|
92
|
-
...(before === null
|
|
93
|
-
? {}
|
|
94
|
-
: { added: added.length, removed: removed.length, newlineChanged }),
|
|
85
|
+
...(before === null ? {} : { added: diff.added, removed: diff.removed, newlineChanged }),
|
|
95
86
|
},
|
|
96
87
|
},
|
|
97
88
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { structuredPatch } from 'diff'
|
|
2
|
+
import type { ToolDiffHunk } from '../../types/tool/presentation.js'
|
|
3
|
+
|
|
4
|
+
/** Lines of unchanged text kept around each change, as `git diff` does. */
|
|
5
|
+
const CONTEXT_LINES = 3
|
|
6
|
+
/** A diff that takes longer than this is abandoned for the coarse count below. */
|
|
7
|
+
const DIFF_TIMEOUT_MS = 5000
|
|
8
|
+
/** Most hunk rows a result carries; the counts always cover the whole change. */
|
|
9
|
+
export const MAX_CARRIED_DIFF_LINES = 400
|
|
10
|
+
/** A single carried row is cut here so one minified line cannot bloat an event. */
|
|
11
|
+
const MAX_CARRIED_LINE_CHARS = 500
|
|
12
|
+
|
|
13
|
+
export interface LineDiff {
|
|
14
|
+
/** Lines added and removed, counted from the hunks of a real line diff. */
|
|
15
|
+
readonly added: number
|
|
16
|
+
readonly removed: number
|
|
17
|
+
/** Hunks with three lines of context; rows are ` `, `+` or `-` followed by the line. */
|
|
18
|
+
readonly hunks: readonly ToolDiffHunk[]
|
|
19
|
+
/** Rows were left out of `hunks` to keep the result small. */
|
|
20
|
+
readonly truncated: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Cut a row without splitting a surrogate pair. */
|
|
24
|
+
function cutRow(row: string): string {
|
|
25
|
+
if (row.length <= MAX_CARRIED_LINE_CHARS) return row
|
|
26
|
+
const cut = row.slice(0, MAX_CARRIED_LINE_CHARS - 1).replace(/[\uD800-\uDBFF]$/, '')
|
|
27
|
+
return `${cut}…`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function splitKeepingTerminators(text: string): string[] {
|
|
31
|
+
return text.length === 0 ? [] : (text.match(/[^\n]*\n|[^\n]+$/g) ?? [])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The coarse fallback when the diff cannot finish in time: everything between the
|
|
36
|
+
* common head and tail counts as changed. Never used when a real diff exists.
|
|
37
|
+
*/
|
|
38
|
+
function coarseDiff(before: string, after: string): LineDiff {
|
|
39
|
+
const a = splitKeepingTerminators(before)
|
|
40
|
+
const b = splitKeepingTerminators(after)
|
|
41
|
+
let head = 0
|
|
42
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head++
|
|
43
|
+
let tail = 0
|
|
44
|
+
while (
|
|
45
|
+
tail < a.length - head &&
|
|
46
|
+
tail < b.length - head &&
|
|
47
|
+
a[a.length - 1 - tail] === b[b.length - 1 - tail]
|
|
48
|
+
)
|
|
49
|
+
tail++
|
|
50
|
+
const removed = a.slice(head, a.length - tail)
|
|
51
|
+
const added = b.slice(head, b.length - tail)
|
|
52
|
+
const rows = [
|
|
53
|
+
...removed.map((line) => `-${line.replace(/\n$/, '')}`),
|
|
54
|
+
...added.map((line) => `+${line.replace(/\n$/, '')}`),
|
|
55
|
+
]
|
|
56
|
+
const kept = rows.slice(0, MAX_CARRIED_DIFF_LINES).map(cutRow)
|
|
57
|
+
return {
|
|
58
|
+
added: added.length,
|
|
59
|
+
removed: removed.length,
|
|
60
|
+
hunks:
|
|
61
|
+
rows.length === 0
|
|
62
|
+
? []
|
|
63
|
+
: [
|
|
64
|
+
{
|
|
65
|
+
oldStart: head + 1,
|
|
66
|
+
oldLines: removed.length,
|
|
67
|
+
newStart: head + 1,
|
|
68
|
+
newLines: added.length,
|
|
69
|
+
lines: kept,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
truncated: rows.length > kept.length,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The real line diff between two bodies, the way git counts it.
|
|
78
|
+
*
|
|
79
|
+
* A line ends at its newline, so a body whose only difference is the final
|
|
80
|
+
* newline changes one line on each side (`+1 -1`), never nothing. The counts
|
|
81
|
+
* come from the hunks of a line diff, not from the span between the first and
|
|
82
|
+
* last changed line, so two edits far apart in one file are two lines, not the
|
|
83
|
+
* whole stretch between them.
|
|
84
|
+
*/
|
|
85
|
+
export function lineDiff(before: string, after: string): LineDiff {
|
|
86
|
+
if (before === after) return { added: 0, removed: 0, hunks: [], truncated: false }
|
|
87
|
+
const patch = structuredPatch('a', 'a', before, after, undefined, undefined, {
|
|
88
|
+
context: CONTEXT_LINES,
|
|
89
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
90
|
+
})
|
|
91
|
+
if (patch === undefined) return coarseDiff(before, after)
|
|
92
|
+
let added = 0
|
|
93
|
+
let removed = 0
|
|
94
|
+
let budget = MAX_CARRIED_DIFF_LINES
|
|
95
|
+
let truncated = false
|
|
96
|
+
const hunks: ToolDiffHunk[] = []
|
|
97
|
+
for (const hunk of patch.hunks) {
|
|
98
|
+
const rows: string[] = []
|
|
99
|
+
for (const row of hunk.lines) {
|
|
100
|
+
// "" describes the row above it; it is not a line.
|
|
101
|
+
if (row.startsWith('\\')) continue
|
|
102
|
+
if (row.startsWith('+')) added++
|
|
103
|
+
else if (row.startsWith('-')) removed++
|
|
104
|
+
if (budget > 0) {
|
|
105
|
+
rows.push(cutRow(row))
|
|
106
|
+
budget--
|
|
107
|
+
} else {
|
|
108
|
+
truncated = true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (rows.length > 0) {
|
|
112
|
+
hunks.push({
|
|
113
|
+
oldStart: hunk.oldStart,
|
|
114
|
+
oldLines: hunk.oldLines,
|
|
115
|
+
newStart: hunk.newStart,
|
|
116
|
+
newLines: hunk.newLines,
|
|
117
|
+
lines: rows,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { added, removed, hunks, truncated }
|
|
122
|
+
}
|
package/src/turn/reporter.ts
CHANGED
|
@@ -442,6 +442,7 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
|
|
|
442
442
|
case 'hosted_tool':
|
|
443
443
|
case 'tool_calls_admitted':
|
|
444
444
|
case 'compaction_shed':
|
|
445
|
+
case 'compaction_started':
|
|
445
446
|
// Deliberately silent. The report is what a human reads about a
|
|
446
447
|
// run; replaying every shed message into it would bury the
|
|
447
448
|
// summary line that says the pass happened, in exactly the
|
|
@@ -184,6 +184,28 @@ type CoreSessionEvent =
|
|
|
184
184
|
/** Automatic threshold, provider rejection, or a host-requested pass. */
|
|
185
185
|
reason: 'threshold' | 'overflow' | 'manual'
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* An automatic compaction pass is about to run.
|
|
189
|
+
*
|
|
190
|
+
* Summarising can take many seconds and every other compaction event
|
|
191
|
+
* arrives AFTER the work, so a host had nothing to show while the turn
|
|
192
|
+
* sat silent. This one arrives first. It is always followed by exactly
|
|
193
|
+
* one of `compaction_completed`, `compaction_failed` or
|
|
194
|
+
* `compaction_tool_results_cleared` (when clearing alone made room), so
|
|
195
|
+
* a host can show a live line and drop it on any of the three.
|
|
196
|
+
*/
|
|
197
|
+
| {
|
|
198
|
+
type: 'compaction_started'
|
|
199
|
+
sessionId: SessionId
|
|
200
|
+
turnId?: TurnId
|
|
201
|
+
iteration: number
|
|
202
|
+
/** `overflow` when the provider rejected the prompt, `threshold` when the trigger fired. */
|
|
203
|
+
reason: 'threshold' | 'overflow'
|
|
204
|
+
/** Context size in tokens that the pass is trying to reduce. */
|
|
205
|
+
tokensBefore: number
|
|
206
|
+
/** The window the trigger measured against. */
|
|
207
|
+
contextWindowTokens: number
|
|
208
|
+
}
|
|
187
209
|
| {
|
|
188
210
|
type: 'compaction_completed'
|
|
189
211
|
sessionId: SessionId
|
|
@@ -1274,6 +1296,9 @@ export type SessionEventListener = (event: SessionEvent) => void | Promise<void>
|
|
|
1274
1296
|
* runtime-context message or tool result that carried it, and a noisy watch
|
|
1275
1297
|
* would write tens of megabytes an hour of raw process output a second time.
|
|
1276
1298
|
* `background_job_exited.monitor` is the durable record of how a watch ended.
|
|
1299
|
+
* `compaction_started` is ephemeral too: it only says a pass is about to run,
|
|
1300
|
+
* and the `compaction` record or `compaction_failed` that ends it is the
|
|
1301
|
+
* durable fact.
|
|
1277
1302
|
*/
|
|
1278
1303
|
export const EPHEMERAL_EVENT_TYPES: ReadonlySet<SessionEventType> = new Set<SessionEventType>([
|
|
1279
1304
|
'text_delta',
|
|
@@ -1281,14 +1306,20 @@ export const EPHEMERAL_EVENT_TYPES: ReadonlySet<SessionEventType> = new Set<Sess
|
|
|
1281
1306
|
'reasoning_delta',
|
|
1282
1307
|
'tool_progress',
|
|
1283
1308
|
'monitor_event',
|
|
1309
|
+
'compaction_started',
|
|
1284
1310
|
])
|
|
1285
1311
|
|
|
1286
1312
|
export function isEphemeralEvent(event: { readonly type: string }): boolean {
|
|
1287
1313
|
return EPHEMERAL_EVENT_TYPES.has(event.type as SessionEventType)
|
|
1288
1314
|
}
|
|
1289
1315
|
|
|
1290
|
-
/** The event types a session log records: every literal except the ephemeral
|
|
1316
|
+
/** The event types a session log records: every literal except the ephemeral six. */
|
|
1291
1317
|
export type PersistedSessionEventType = Exclude<
|
|
1292
1318
|
SessionEventType,
|
|
1293
|
-
|
|
1319
|
+
| 'text_delta'
|
|
1320
|
+
| 'tool_input_delta'
|
|
1321
|
+
| 'reasoning_delta'
|
|
1322
|
+
| 'tool_progress'
|
|
1323
|
+
| 'monitor_event'
|
|
1324
|
+
| 'compaction_started'
|
|
1294
1325
|
>
|
|
@@ -629,6 +629,7 @@ const SESSION_EVENT_TYPE_TABLE: Record<SessionEventType, true> = {
|
|
|
629
629
|
approval_policy_changed: true,
|
|
630
630
|
request_envelope: true,
|
|
631
631
|
iteration_completed: true,
|
|
632
|
+
compaction_started: true,
|
|
632
633
|
compaction_shed: true,
|
|
633
634
|
compaction_completed: true,
|
|
634
635
|
compaction_tool_results_cleared: true,
|
|
@@ -688,12 +689,12 @@ const SESSION_EVENT_TYPE_TABLE: Record<SessionEventType, true> = {
|
|
|
688
689
|
child_session_idled: true,
|
|
689
690
|
}
|
|
690
691
|
|
|
691
|
-
/** All
|
|
692
|
+
/** All 64 live event type literals. */
|
|
692
693
|
export const SESSION_EVENT_TYPES: readonly SessionEventType[] = Object.freeze(
|
|
693
694
|
Object.keys(SESSION_EVENT_TYPE_TABLE) as SessionEventType[],
|
|
694
695
|
)
|
|
695
696
|
|
|
696
|
-
/** The 58 event types a session log records: all
|
|
697
|
+
/** The 58 event types a session log records: all 64 but the six ephemeral ones. */
|
|
697
698
|
export const PERSISTED_SESSION_EVENT_TYPES: readonly PersistedSessionEventType[] = Object.freeze(
|
|
698
699
|
SESSION_EVENT_TYPES.filter(
|
|
699
700
|
(type) => !EPHEMERAL_EVENT_TYPES.has(type),
|
package/src/types/tool/index.ts
CHANGED
|
@@ -82,6 +82,19 @@ export interface ToolTextWindow {
|
|
|
82
82
|
readonly totalBytes: number
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* One hunk of a line diff: the changed lines with up to three unchanged lines
|
|
87
|
+
* around them. Every row of `lines` is a space, `+` or `-` followed by the
|
|
88
|
+
* line's text, without its newline. Line numbers start at 1.
|
|
89
|
+
*/
|
|
90
|
+
export interface ToolDiffHunk {
|
|
91
|
+
readonly oldStart: number
|
|
92
|
+
readonly oldLines: number
|
|
93
|
+
readonly newStart: number
|
|
94
|
+
readonly newLines: number
|
|
95
|
+
readonly lines: readonly string[]
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
/** Why a call did not do what it was asked, as a kind a host can phrase for a person. */
|
|
86
99
|
export type ToolFailureReason =
|
|
87
100
|
| 'not_found'
|
|
@@ -154,6 +167,13 @@ export type ToolFacts =
|
|
|
154
167
|
readonly kind: 'edit'
|
|
155
168
|
readonly path?: string
|
|
156
169
|
readonly replacements?: number
|
|
170
|
+
/** Lines added and removed, counted from a real line diff of the whole file. */
|
|
171
|
+
readonly added?: number
|
|
172
|
+
readonly removed?: number
|
|
173
|
+
/** The change as hunks with three lines of context. Capped; see `hunksTruncated`. */
|
|
174
|
+
readonly hunks?: readonly ToolDiffHunk[]
|
|
175
|
+
/** Some rows of the change are not in `hunks`; `added` and `removed` still cover all of it. */
|
|
176
|
+
readonly hunksTruncated?: true
|
|
157
177
|
}
|
|
158
178
|
| {
|
|
159
179
|
readonly kind: 'write'
|
|
@@ -163,6 +183,9 @@ export type ToolFacts =
|
|
|
163
183
|
readonly bytes?: number
|
|
164
184
|
readonly added?: number
|
|
165
185
|
readonly removed?: number
|
|
186
|
+
/** The change as hunks; a created file is one hunk of added lines. Capped; see `hunksTruncated`. */
|
|
187
|
+
readonly hunks?: readonly ToolDiffHunk[]
|
|
188
|
+
readonly hunksTruncated?: true
|
|
166
189
|
}
|
|
167
190
|
| {
|
|
168
191
|
readonly kind: 'failure'
|