@namzu/sdk 52.6.2 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +1 -0
  4. package/dist/bridge/a2a/mapper.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +9 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/contracts/session/api.d.ts +1 -1
  9. package/dist/contracts/session/api.d.ts.map +1 -1
  10. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  11. package/dist/execution/workflow/prelude.js +12 -2
  12. package/dist/execution/workflow/prelude.js.map +1 -1
  13. package/dist/runtime/query/iteration/phases/compaction.js +10 -0
  14. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  15. package/dist/tools/admit-facts.d.ts.map +1 -1
  16. package/dist/tools/admit-facts.js +42 -0
  17. package/dist/tools/admit-facts.js.map +1 -1
  18. package/dist/tools/builtins/edit.d.ts.map +1 -1
  19. package/dist/tools/builtins/edit.js +27 -12
  20. package/dist/tools/builtins/edit.js.map +1 -1
  21. package/dist/tools/builtins/file-write-result.d.ts.map +1 -1
  22. package/dist/tools/builtins/file-write-result.js +24 -31
  23. package/dist/tools/builtins/file-write-result.js.map +1 -1
  24. package/dist/tools/builtins/line-diff.d.ts +23 -0
  25. package/dist/tools/builtins/line-diff.d.ts.map +1 -0
  26. package/dist/tools/builtins/line-diff.js +112 -0
  27. package/dist/tools/builtins/line-diff.js.map +1 -0
  28. package/dist/turn/reporter.d.ts.map +1 -1
  29. package/dist/turn/reporter.js +1 -0
  30. package/dist/turn/reporter.js.map +1 -1
  31. package/dist/types/session/events.d.ts +27 -2
  32. package/dist/types/session/events.d.ts.map +1 -1
  33. package/dist/types/session/events.js +4 -0
  34. package/dist/types/session/events.js.map +1 -1
  35. package/dist/types/session/records.d.ts +2 -2
  36. package/dist/types/session/records.d.ts.map +1 -1
  37. package/dist/types/session/records.js +3 -2
  38. package/dist/types/session/records.js.map +1 -1
  39. package/dist/types/tool/index.d.ts +1 -1
  40. package/dist/types/tool/index.d.ts.map +1 -1
  41. package/dist/types/tool/presentation.d.ts +22 -0
  42. package/dist/types/tool/presentation.d.ts.map +1 -1
  43. package/package.json +2 -1
  44. package/src/bridge/a2a/mapper.ts +1 -0
  45. package/src/bridge/sse/mapper.ts +9 -0
  46. package/src/contracts/session/api.ts +1 -0
  47. package/src/execution/workflow/prelude.ts +12 -2
  48. package/src/runtime/query/iteration/phases/compaction.ts +11 -0
  49. package/src/tools/admit-facts.ts +46 -1
  50. package/src/tools/builtins/edit.ts +28 -12
  51. package/src/tools/builtins/file-write-result.ts +25 -34
  52. package/src/tools/builtins/line-diff.ts +122 -0
  53. package/src/turn/reporter.ts +1 -0
  54. package/src/types/session/events.ts +33 -2
  55. package/src/types/session/records.ts +3 -2
  56. package/src/types/tool/index.ts +1 -0
  57. 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
- /** A final newline terminates a line; it does not add an empty line. */
5
- function lines(text: string): string[] {
6
- if (text === '') return []
7
- const result = text.split('\n')
8
- if (result.at(-1) === '') result.pop()
9
- return result
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
- const oldLines = lines(before ?? '')
28
- const newLines = lines(after)
29
- let start = 0
30
- let end = 0
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.length} -${removed.length})`}${newlineChanged ? ' · final newline changed' : ''}`
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
- ...(before === null ? {} : { added: added.length, removed: removed.length }),
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: removed.length ? `${removed.join('\n')}\n` : '',
87
- after: added.length ? `${added.join('\n')}\n` : '',
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
+ }
@@ -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 five. */
1316
+ /** The event types a session log records: every literal except the ephemeral six. */
1291
1317
  export type PersistedSessionEventType = Exclude<
1292
1318
  SessionEventType,
1293
- 'text_delta' | 'tool_input_delta' | 'reasoning_delta' | 'tool_progress' | 'monitor_event'
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 63 live event type literals. */
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 63 but the five ephemeral ones. */
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),
@@ -1331,6 +1331,7 @@ export * from './repair.js'
1331
1331
 
1332
1332
  export type {
1333
1333
  ToolCallView,
1334
+ ToolDiffHunk,
1334
1335
  ToolFacts,
1335
1336
  ToolFailureReason,
1336
1337
  ToolPresentation,
@@ -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'