@namzu/sdk 37.0.0 → 38.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +2 -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 +14 -1
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/config/runtime.d.ts +16 -6
  9. package/dist/config/runtime.d.ts.map +1 -1
  10. package/dist/config/runtime.js +2 -0
  11. package/dist/config/runtime.js.map +1 -1
  12. package/dist/contracts/api.d.ts +1 -1
  13. package/dist/contracts/api.d.ts.map +1 -1
  14. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  15. package/dist/manager/agent/lifecycle.js +10 -9
  16. package/dist/manager/agent/lifecycle.js.map +1 -1
  17. package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
  18. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  19. package/dist/prompt/coding-agent-doctrine.js +11 -5
  20. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  21. package/dist/provider/capabilities.d.ts +2 -0
  22. package/dist/provider/capabilities.d.ts.map +1 -1
  23. package/dist/provider/capabilities.js +11 -0
  24. package/dist/provider/capabilities.js.map +1 -1
  25. package/dist/provider/fallback.d.ts.map +1 -1
  26. package/dist/provider/fallback.js +5 -3
  27. package/dist/provider/fallback.js.map +1 -1
  28. package/dist/public-runtime.d.ts +1 -0
  29. package/dist/public-runtime.d.ts.map +1 -1
  30. package/dist/public-runtime.js +1 -0
  31. package/dist/public-runtime.js.map +1 -1
  32. package/dist/public-tools.d.ts +1 -0
  33. package/dist/public-tools.d.ts.map +1 -1
  34. package/dist/public-tools.js +1 -0
  35. package/dist/public-tools.js.map +1 -1
  36. package/dist/public-types.d.ts +1 -0
  37. package/dist/public-types.d.ts.map +1 -1
  38. package/dist/run/reporter.d.ts.map +1 -1
  39. package/dist/run/reporter.js +1 -0
  40. package/dist/run/reporter.js.map +1 -1
  41. package/dist/run/token-budget.d.ts +10 -4
  42. package/dist/run/token-budget.d.ts.map +1 -1
  43. package/dist/run/token-budget.js +45 -14
  44. package/dist/run/token-budget.js.map +1 -1
  45. package/dist/runtime/query/executor.d.ts +3 -3
  46. package/dist/runtime/query/executor.d.ts.map +1 -1
  47. package/dist/runtime/query/executor.js +24 -16
  48. package/dist/runtime/query/executor.js.map +1 -1
  49. package/dist/runtime/query/index.d.ts +2 -0
  50. package/dist/runtime/query/index.d.ts.map +1 -1
  51. package/dist/runtime/query/index.js +1 -0
  52. package/dist/runtime/query/index.js.map +1 -1
  53. package/dist/runtime/query/iteration/index.d.ts +2 -0
  54. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  55. package/dist/runtime/query/iteration/index.js +23 -2
  56. package/dist/runtime/query/iteration/index.js.map +1 -1
  57. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  58. package/dist/runtime/query/iteration/stream-turn.js +11 -1
  59. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  60. package/dist/runtime/query/observation-context.d.ts +10 -0
  61. package/dist/runtime/query/observation-context.d.ts.map +1 -0
  62. package/dist/runtime/query/observation-context.js +84 -0
  63. package/dist/runtime/query/observation-context.js.map +1 -0
  64. package/dist/runtime/query/request-context.d.ts +30 -0
  65. package/dist/runtime/query/request-context.d.ts.map +1 -0
  66. package/dist/runtime/query/request-context.js +76 -0
  67. package/dist/runtime/query/request-context.js.map +1 -0
  68. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  69. package/dist/runtime/query/request-rich-content.js +6 -3
  70. package/dist/runtime/query/request-rich-content.js.map +1 -1
  71. package/dist/runtime/query/review-policy.d.ts +3 -0
  72. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  73. package/dist/runtime/query/review-policy.js +4 -1
  74. package/dist/runtime/query/review-policy.js.map +1 -1
  75. package/dist/runtime/query/tooling.d.ts +1 -0
  76. package/dist/runtime/query/tooling.d.ts.map +1 -1
  77. package/dist/runtime/query/tooling.js +1 -0
  78. package/dist/runtime/query/tooling.js.map +1 -1
  79. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  80. package/dist/sandbox/file-walk-program.js +8 -0
  81. package/dist/sandbox/file-walk-program.js.map +1 -1
  82. package/dist/sandbox/file-walk.d.ts.map +1 -1
  83. package/dist/sandbox/file-walk.js +9 -0
  84. package/dist/sandbox/file-walk.js.map +1 -1
  85. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  86. package/dist/scheduler/completion-inbox.js +3 -0
  87. package/dist/scheduler/completion-inbox.js.map +1 -1
  88. package/dist/store/run/token-budget-disk.js +3 -0
  89. package/dist/store/run/token-budget-disk.js.map +1 -1
  90. package/dist/tools/builtins/edit.d.ts.map +1 -1
  91. package/dist/tools/builtins/edit.js +12 -26
  92. package/dist/tools/builtins/edit.js.map +1 -1
  93. package/dist/tools/builtins/file-write-result.d.ts +4 -0
  94. package/dist/tools/builtins/file-write-result.d.ts.map +1 -0
  95. package/dist/tools/builtins/file-write-result.js +72 -0
  96. package/dist/tools/builtins/file-write-result.js.map +1 -0
  97. package/dist/tools/builtins/glob.d.ts.map +1 -1
  98. package/dist/tools/builtins/glob.js +8 -1
  99. package/dist/tools/builtins/glob.js.map +1 -1
  100. package/dist/tools/builtins/grep.d.ts.map +1 -1
  101. package/dist/tools/builtins/grep.js +10 -0
  102. package/dist/tools/builtins/grep.js.map +1 -1
  103. package/dist/tools/builtins/job.d.ts.map +1 -1
  104. package/dist/tools/builtins/job.js +6 -0
  105. package/dist/tools/builtins/job.js.map +1 -1
  106. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  107. package/dist/tools/builtins/read-file.js +10 -0
  108. package/dist/tools/builtins/read-file.js.map +1 -1
  109. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  110. package/dist/tools/builtins/write-file.js +30 -44
  111. package/dist/tools/builtins/write-file.js.map +1 -1
  112. package/dist/tools/file-read-tracker.d.ts +4 -0
  113. package/dist/tools/file-read-tracker.d.ts.map +1 -0
  114. package/dist/tools/file-read-tracker.js +16 -0
  115. package/dist/tools/file-read-tracker.js.map +1 -0
  116. package/dist/tools/task/list.js +2 -2
  117. package/dist/tools/task/list.js.map +1 -1
  118. package/dist/types/plugin/index.d.ts +7 -0
  119. package/dist/types/plugin/index.d.ts.map +1 -1
  120. package/dist/types/plugin/index.js.map +1 -1
  121. package/dist/types/provider/chat.d.ts +4 -0
  122. package/dist/types/provider/chat.d.ts.map +1 -1
  123. package/dist/types/provider/config.d.ts +2 -0
  124. package/dist/types/provider/config.d.ts.map +1 -1
  125. package/dist/types/provider/stream.d.ts +6 -0
  126. package/dist/types/provider/stream.d.ts.map +1 -1
  127. package/dist/types/run/config.d.ts +2 -0
  128. package/dist/types/run/config.d.ts.map +1 -1
  129. package/dist/types/run/events.d.ts +10 -0
  130. package/dist/types/run/events.d.ts.map +1 -1
  131. package/dist/types/run/events.js.map +1 -1
  132. package/dist/types/sandbox/index.d.ts +1 -0
  133. package/dist/types/sandbox/index.d.ts.map +1 -1
  134. package/dist/types/sandbox/index.js.map +1 -1
  135. package/dist/types/tool/presentation.d.ts +4 -0
  136. package/dist/types/tool/presentation.d.ts.map +1 -1
  137. package/package.json +1 -1
  138. package/src/bridge/a2a/mapper.ts +2 -0
  139. package/src/bridge/sse/mapper.ts +14 -1
  140. package/src/config/runtime.ts +2 -0
  141. package/src/contracts/api.ts +1 -0
  142. package/src/manager/agent/lifecycle.ts +12 -9
  143. package/src/prompt/coding-agent-doctrine.ts +11 -5
  144. package/src/provider/capabilities.ts +16 -0
  145. package/src/provider/fallback.ts +8 -3
  146. package/src/public-runtime.ts +2 -0
  147. package/src/public-tools.ts +2 -0
  148. package/src/public-types.ts +6 -0
  149. package/src/run/reporter.ts +1 -0
  150. package/src/run/token-budget.ts +51 -14
  151. package/src/runtime/query/executor.ts +23 -15
  152. package/src/runtime/query/index.ts +3 -0
  153. package/src/runtime/query/iteration/index.ts +28 -2
  154. package/src/runtime/query/iteration/stream-turn.ts +14 -1
  155. package/src/runtime/query/observation-context.ts +86 -0
  156. package/src/runtime/query/request-context.ts +103 -0
  157. package/src/runtime/query/request-rich-content.ts +8 -3
  158. package/src/runtime/query/review-policy.ts +7 -1
  159. package/src/runtime/query/tooling.ts +2 -0
  160. package/src/sandbox/file-walk-program.ts +8 -0
  161. package/src/sandbox/file-walk.ts +9 -0
  162. package/src/scheduler/completion-inbox.ts +3 -0
  163. package/src/store/run/token-budget-disk.ts +3 -0
  164. package/src/tools/builtins/edit.ts +12 -27
  165. package/src/tools/builtins/file-write-result.ts +84 -0
  166. package/src/tools/builtins/glob.ts +7 -1
  167. package/src/tools/builtins/grep.ts +10 -0
  168. package/src/tools/builtins/job.ts +8 -0
  169. package/src/tools/builtins/read-file.ts +10 -0
  170. package/src/tools/builtins/write-file.ts +39 -44
  171. package/src/tools/file-read-tracker.ts +16 -0
  172. package/src/tools/task/list.ts +2 -2
  173. package/src/types/plugin/index.ts +10 -0
  174. package/src/types/provider/chat.ts +2 -0
  175. package/src/types/provider/config.ts +2 -0
  176. package/src/types/provider/stream.ts +7 -0
  177. package/src/types/run/config.ts +2 -0
  178. package/src/types/run/events.ts +10 -0
  179. package/src/types/sandbox/index.ts +1 -0
  180. package/src/types/tool/presentation.ts +4 -0
@@ -227,6 +227,15 @@ async function* executeWalk(
227
227
  }
228
228
  if (canonicalRoot !== path.resolve(plan.root))
229
229
  throw new Error('File walk root follows a symbolic link; use its authorized real directory')
230
+ // A named regular file is a one-entry search, not an empty directory.
231
+ const rootInfo = await wait(fs.lstat(plan.root))
232
+ check()
233
+ if (rootInfo.isFile()) {
234
+ noteVisit()
235
+ if (matchers.some((matcher) => matcher.test(path.basename(plan.root))))
236
+ yield { path: plan.root, size: rootInfo.size }
237
+ return
238
+ }
230
239
  let start = plan.root
231
240
  // Never follow a static-prefix symlink to speed up a pattern: traversal
232
241
  // and its optimized starting point must have the same link policy.
@@ -464,6 +464,9 @@ export function formatCompletionNotification(handles: readonly TaskHandle[]): st
464
464
  `task_id: ${handle.taskId}`,
465
465
  `agent: ${handle.agentId}`,
466
466
  `state: ${handle.state}`,
467
+ ...(handle.state === 'completed' && run?.stopReason && run.stopReason !== 'end_turn'
468
+ ? ['outcome: incomplete — execution stopped; this does not establish task completion.']
469
+ : []),
467
470
  ...(handle.result?.stopReason ? [`stop_reason: ${handle.result.stopReason}`] : []),
468
471
  ...(durationMs !== undefined ? [`duration_ms: ${durationMs}`] : []),
469
472
  '',
@@ -227,6 +227,9 @@ function assertSameRoot(before: TokenBudgetSnapshot, after: TokenBudgetSnapshot)
227
227
  !current ||
228
228
  current.accountId !== previous.accountId ||
229
229
  current.runId !== previous.runId ||
230
+ (previous.unresolved === true &&
231
+ current.unresolved !== true &&
232
+ (active.has(previous.id) || current.usage === undefined)) ||
230
233
  (reconciled && current.usage === undefined) ||
231
234
  !usageAtLeast(current.usage, previous.usage)
232
235
  ) {
@@ -362,12 +362,18 @@ export const EditTool = defineTool({
362
362
  return withFileMutationLock(lockKey, async () => {
363
363
  if (context.sandbox) {
364
364
  const buffer = await context.sandbox.readFile(parsed.data.path)
365
- const result = applyEdit(buffer.toString('utf-8'), normalized.operations)
365
+ const content = buffer.toString('utf-8')
366
+ const seen = context.fileReadTracker?.fingerprint?.(parsed.data.path)
367
+ if (seen !== undefined && seen !== fingerprintContent(content)) {
368
+ return { success: false as const, output: '', error: staleFileError(parsed.data.path) }
369
+ }
370
+ const result = applyEdit(content, normalized.operations)
366
371
  if (!result.success) {
367
372
  return { success: false as const, output: '', error: result.error }
368
373
  }
369
374
 
370
375
  await context.sandbox.writeFile(parsed.data.path, result.content)
376
+ context.fileReadTracker?.recordRead(parsed.data.path, result.content)
371
377
  return {
372
378
  success: true as const,
373
379
  output: `Edited ${parsed.data.path}: ${result.replacements} replacement(s) [sandboxed]`,
@@ -378,33 +384,12 @@ export const EditTool = defineTool({
378
384
  const hostPath = filePath as string
379
385
  const content = await readFile(hostPath, 'utf-8')
380
386
 
381
- const result = applyEdit(content, normalized.operations)
382
- if (!result.success) {
383
- // The anchor did not match what is on disk. Two very different
384
- // situations produce that, and the difference is the whole
385
- // value of the fingerprint:
386
- //
387
- // The file is as the agent left it, and the anchor is simply
388
- // wrong — "not found, match the whitespace exactly" is the
389
- // right thing to say.
390
- //
391
- // Or the file moved after the read — a person in an editor,
392
- // another process, a second agent — and the same message is a
393
- // lie that sends the agent to re-check text that was never the
394
- // problem, so it retries the identical edit against the same
395
- // moved file.
396
- //
397
- // Deliberately NOT a gate on the successful path. An anchor
398
- // that still matches uniquely is well defined however much
399
- // changed elsewhere in the file, and refusing there would
400
- // reject safe edits every time anyone touched an unrelated
401
- // line.
402
- const seen = context.fileReadTracker?.fingerprint?.(hostPath)
403
- if (seen !== undefined && seen !== fingerprintContent(content)) {
404
- return { success: false as const, output: '', error: staleFileError(hostPath) }
405
- }
406
- return { success: false as const, output: '', error: result.error }
387
+ const seen = context.fileReadTracker?.fingerprint?.(hostPath)
388
+ if (seen !== undefined && seen !== fingerprintContent(content)) {
389
+ return { success: false as const, output: '', error: staleFileError(hostPath) }
407
390
  }
391
+ const result = applyEdit(content, normalized.operations)
392
+ if (!result.success) return { success: false as const, output: '', error: result.error }
408
393
 
409
394
  // Temp file, fsync, rename — a reader sees the old body or the new
410
395
  // one, never a half-written one. A plain `writeFile` that fails
@@ -0,0 +1,84 @@
1
+ import { createHash } from 'node:crypto'
2
+ import type { ToolResult } from '../../types/tool/index.js'
3
+
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
10
+ }
11
+
12
+ /** null means the backend could not establish the previous state. */
13
+ export function fileWriteResult(
14
+ path: string,
15
+ before: string | undefined | null,
16
+ after: string,
17
+ sandboxed = false,
18
+ ): ToolResult {
19
+ const operation =
20
+ before === null
21
+ ? 'write'
22
+ : before === undefined
23
+ ? 'create'
24
+ : before === after
25
+ ? 'unchanged'
26
+ : '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)
47
+ const bytes = Buffer.byteLength(after, 'utf8')
48
+ const label =
49
+ operation === 'create'
50
+ ? 'Created'
51
+ : operation === 'replace'
52
+ ? 'Updated'
53
+ : operation === 'unchanged'
54
+ ? 'Unchanged'
55
+ : 'Wrote'
56
+ const newlineChanged =
57
+ typeof before === 'string' && before.endsWith('\n') !== after.endsWith('\n')
58
+ const summary = `${label} ${path}${before === null ? '' : ` (+${added.length} -${removed.length})`}${newlineChanged ? ' · final newline changed' : ''}`
59
+ return {
60
+ success: true,
61
+ output: `${summary} · ${bytes} bytes`,
62
+ data: {
63
+ path,
64
+ size: after.length,
65
+ ...(sandboxed ? { sandboxed: true } : {}),
66
+ fileChange: {
67
+ operation,
68
+ ...(before === null
69
+ ? {}
70
+ : {
71
+ preview: {
72
+ before: removed.length ? `${removed.join('\n')}\n` : '',
73
+ after: added.length ? `${added.join('\n')}\n` : '',
74
+ },
75
+ }),
76
+ bytes,
77
+ sha256: createHash('sha256').update(after, 'utf8').digest('hex'),
78
+ ...(before === null
79
+ ? {}
80
+ : { added: added.length, removed: removed.length, newlineChanged }),
81
+ },
82
+ },
83
+ }
84
+ }
@@ -68,7 +68,13 @@ export const GlobTool = defineTool({
68
68
  timeoutMs: 15_000,
69
69
 
70
70
  presentCall(input) {
71
- return { kind: 'generic', label: `${input.pattern} in ${input.path ?? '.'}` }
71
+ if (typeof input.pattern !== 'string') return undefined
72
+ return {
73
+ kind: 'generic',
74
+ label: `Find ${input.pattern} in ${input.path ?? '.'}`,
75
+ presentation: 'activity',
76
+ activity: 'exploration',
77
+ }
72
78
  },
73
79
 
74
80
  async execute(input, context) {
@@ -98,6 +98,16 @@ export const GrepTool = defineTool({
98
98
  readOnly: true,
99
99
  destructive: false,
100
100
  concurrencySafe: true,
101
+
102
+ presentCall(input) {
103
+ if (typeof input.pattern !== 'string') return undefined
104
+ return {
105
+ kind: 'generic',
106
+ label: `Search ${input.pattern} in ${input.path ?? '.'}`,
107
+ presentation: 'activity',
108
+ activity: 'exploration',
109
+ }
110
+ },
101
111
  timeoutMs: 15_000,
102
112
 
103
113
  async execute(input, context) {
@@ -54,6 +54,14 @@ export const JobTool = defineTool({
54
54
  destructive: (input: JobInput) => input.action === 'kill',
55
55
  concurrencySafe: true,
56
56
 
57
+ presentCall(input) {
58
+ const label =
59
+ input.action === 'list'
60
+ ? 'List background jobs'
61
+ : `${input.action === 'kill' ? 'Stop background job' : 'Read background output'} · ${input.id ?? '(missing id)'}`
62
+ return { kind: 'generic', presentation: 'activity', label }
63
+ },
64
+
57
65
  async execute(input, context) {
58
66
  if (!context.backgroundJobs) {
59
67
  return {
@@ -32,6 +32,16 @@ export const ReadFileTool = defineTool({
32
32
  destructive: false,
33
33
  concurrencySafe: true,
34
34
 
35
+ presentCall(input) {
36
+ if (typeof input.path !== 'string') return undefined
37
+ return {
38
+ kind: 'generic',
39
+ label: `Read ${input.path}`,
40
+ presentation: 'activity',
41
+ activity: 'exploration',
42
+ }
43
+ },
44
+
35
45
  async execute(input, context) {
36
46
  // Sandbox-aware: route through sandbox.readFile() when available
37
47
  if (context.sandbox) {
@@ -7,6 +7,7 @@ import { resolveWithinAnyReal, toolRoots } from '../paths.js'
7
7
  import { atomicWriteFile } from './atomic-write-file.js'
8
8
  import { fingerprintContent, staleFileError } from './content-fingerprint.js'
9
9
  import { withFileMutationLock } from './file-mutation-lock.js'
10
+ import { fileWriteResult } from './file-write-result.js'
10
11
 
11
12
  const inputSchema = z
12
13
  .object({
@@ -81,40 +82,35 @@ export const WriteFileTool = defineTool({
81
82
  destructive: true,
82
83
  concurrencySafe: false,
83
84
 
84
- /**
85
- * The whole file body, described by the tool.
86
- *
87
- * A `diff` with an EMPTY `before`, which is what a write actually is:
88
- * whatever was there is gone and this is what replaces it. `edit`
89
- * declines to do that for an insert — there, an empty `before` would
90
- * claim the whole file was added and it was not. Here it is true.
91
- *
92
- * A host with nothing to contrast against renders the `after` plainly
93
- * rather than as `+` lines, which is the row a reader wants: they are
94
- * approving a file, not reviewing a patch.
95
- */
85
+ // Input alone cannot establish whether this replaces an existing file.
96
86
  presentCall(input: WriteInput) {
97
- const content = input.content ?? input.newStr
98
- if (typeof content !== 'string') return undefined
99
- return {
100
- kind: 'diff' as const,
101
- ...(input.path ? { path: input.path } : {}),
102
- before: '',
103
- after: content,
104
- }
87
+ return { kind: 'generic' as const, label: input.path }
105
88
  },
106
89
 
107
- /**
108
- * A label, deliberately — which is what suppresses the detail block.
109
- *
110
- * The content was already shown under the CALL, where the user could
111
- * act on it. Repeating it under the result doubles the longest rows in
112
- * the transcript to say nothing new. A host used to decide this by
113
- * matching `name === 'write' || name === 'edit'`; it is the tool's to
114
- * say, and now it says it.
115
- */
90
+ /** The receipt names the observed operation; failures retain their error output. */
116
91
  presentResult(_input: WriteInput, result: ToolResult) {
117
- return { kind: 'generic' as const, label: result.output?.split('\n')[0] ?? '' }
92
+ const data = result.data as
93
+ | { fileChange?: { preview?: { before?: unknown; after?: unknown } } }
94
+ | undefined
95
+ const preview = data?.fileChange?.preview
96
+ if (
97
+ result.success &&
98
+ typeof preview?.before === 'string' &&
99
+ typeof preview.after === 'string'
100
+ ) {
101
+ return {
102
+ kind: 'diff' as const,
103
+ path: _input.path,
104
+ label: result.output?.split('\n')[0],
105
+ before: preview.before,
106
+ after: preview.after,
107
+ }
108
+ }
109
+ return {
110
+ kind: 'generic' as const,
111
+ label: result.output?.split('\n')[0] ?? '',
112
+ ...(result.success ? { presentation: 'activity' as const } : {}),
113
+ }
118
114
  },
119
115
 
120
116
  async execute(input: WriteInput, context) {
@@ -146,7 +142,7 @@ export const WriteFileTool = defineTool({
146
142
  return withFileMutationLock(lockKey, async () => {
147
143
  if (context.sandbox) {
148
144
  const current = await readSandboxFileIfPresent(context, valid.path)
149
- if (current !== undefined) {
145
+ if (current !== undefined && current !== null) {
150
146
  const guard = enforceFreshOverwrite(context, valid.path, current.toString('utf-8'))
151
147
  if (guard) return guard
152
148
  }
@@ -155,11 +151,12 @@ export const WriteFileTool = defineTool({
155
151
  // the boolean would leave an older content fingerprint in place and
156
152
  // make the next same-run write refuse its own predecessor as drift.
157
153
  context.fileReadTracker?.recordRead(valid.path, content)
158
- return {
159
- success: true as const,
160
- output: `File written successfully: ${valid.path} (${content.length} chars) [sandboxed]`,
161
- data: { path: valid.path, size: content.length, sandboxed: true },
162
- }
154
+ return fileWriteResult(
155
+ valid.path,
156
+ current === null ? null : current?.toString('utf-8'),
157
+ content,
158
+ true,
159
+ )
163
160
  }
164
161
 
165
162
  const hostPath = filePath as string
@@ -176,11 +173,7 @@ export const WriteFileTool = defineTool({
176
173
  await atomicWriteFile(hostPath, content)
177
174
  context.fileReadTracker?.recordRead(hostPath, content)
178
175
 
179
- return {
180
- success: true as const,
181
- output: `File written successfully: ${hostPath} (${content.length} chars)`,
182
- data: { path: hostPath, size: content.length },
183
- }
176
+ return fileWriteResult(hostPath, current, content)
184
177
  })
185
178
  },
186
179
  })
@@ -237,14 +230,16 @@ function isMissingFileError(error: unknown): boolean {
237
230
  async function readSandboxFileIfPresent(
238
231
  context: ToolContext,
239
232
  path: string,
240
- ): Promise<Buffer | undefined> {
233
+ ): Promise<Buffer | undefined | null> {
241
234
  if (!context.sandbox) return undefined
242
235
  try {
243
236
  return await context.sandbox.readFile(path)
244
- } catch {
237
+ } catch (error) {
238
+ if (isMissingFileError(error)) return undefined
245
239
  // `Sandbox` currently has no typed missing-file result. Preserve the
246
240
  // existing create behavior for all backends; when a read succeeds, the
247
241
  // exact body above is still a real admission-time freshness check.
248
- return undefined
242
+ // An untyped read failure does not prove that the target was absent.
243
+ return null
249
244
  }
250
245
  }
@@ -0,0 +1,16 @@
1
+ import type { FileReadTracker } from '../types/tool/index.js'
2
+ import { fingerprintContent } from './builtins/content-fingerprint.js'
3
+
4
+ /** Host-owned observation ledger. Share only across turns of the same conversation and filesystem. */
5
+ export function createFileReadTracker(): FileReadTracker {
6
+ const paths = new Set<string>()
7
+ const fingerprints = new Map<string, string>()
8
+ return {
9
+ recordRead(key, content) {
10
+ paths.add(key)
11
+ if (content !== undefined) fingerprints.set(key, fingerprintContent(content))
12
+ },
13
+ hasRead: (key) => paths.has(key),
14
+ fingerprint: (key) => fingerprints.get(key),
15
+ }
16
+ }
@@ -8,7 +8,7 @@ export function buildTaskListTool(taskStore: TaskStore, runId: RunId): ToolDefin
8
8
  return defineTool({
9
9
  name: 'task_list',
10
10
  description:
11
- 'List all tasks for the current run. Shows subject, status, owner, and unresolved blockers. Use this to review your plan and decide what to work on next.',
11
+ 'List planning items for the current run, not delegated agent invocations. Use agent_task_list, when available, to inspect agent execution. Shows subject, status, owner, and unresolved blockers. Use this to review your plan and decide what to work on next.',
12
12
  inputSchema: z.object({}),
13
13
  category: 'custom',
14
14
  permissions: [],
@@ -46,7 +46,7 @@ export function buildTaskListTool(taskStore: TaskStore, runId: RunId): ToolDefin
46
46
  success: true,
47
47
  output:
48
48
  tasks.length === 0
49
- ? 'No tasks yet.'
49
+ ? 'No planning tasks yet. This list does not report delegated agent status; use agent_task_list when available.'
50
50
  : `${stats.total} tasks: ${stats.completed} completed, ${stats.in_progress} in progress, ${stats.pending} pending.`,
51
51
  data: { tasks: summary, stats },
52
52
  }
@@ -8,6 +8,10 @@ import {
8
8
  MAX_TOOLS_PER_PLUGIN,
9
9
  PLUGIN_NAME_MAX_LENGTH,
10
10
  } from '../../constants/plugin/index.js'
11
+ import type {
12
+ RequestContextChange,
13
+ RequestContextSnapshot,
14
+ } from '../../runtime/query/request-context.js'
11
15
  import type { PluginId, RunId, SessionId } from '../ids/index.js'
12
16
  import type { Message, ToolResultContent } from '../message/index.js'
13
17
  import type { CancelCause } from '../run/cancel-cause.js'
@@ -142,6 +146,12 @@ export function assertPluginHookEvent(event: PluginHookEvent): void {
142
146
  * contract by accident.
143
147
  */
144
148
  export interface PluginModelRequest {
149
+ /** SDK provider-input content inventory; excludes adapter/server-private transformations. */
150
+ readonly context?: {
151
+ readonly snapshot: RequestContextSnapshot
152
+ /** Compared with the preceding pre_llm_call in this run; absent on the first. */
153
+ readonly change?: RequestContextChange
154
+ }
145
155
  readonly model: string
146
156
  readonly messages: readonly Message[]
147
157
  /** Names only. A hook auditing tool exposure needs the set, not the schemas. */
@@ -34,6 +34,8 @@ export interface ChatCompletionParams {
34
34
  * calls may omit it; a provider then uses its own id, `model`, and index 0.
35
35
  */
36
36
  providerRoute?: ProviderRoute
37
+ /** Opt in to provider-hosted web search. Absent means no hosted search. */
38
+ webSearch?: { mode: 'cached' | 'live' }
37
39
  tools?: LLMToolSchema[]
38
40
  /**
39
41
  * Provider hint naming tools whose model-facing JSON Schema should be
@@ -121,6 +121,8 @@ export interface MockProviderConfig extends MockScript {
121
121
  * degradation is loud instead of silent.
122
122
  */
123
123
  export interface ProviderCapabilities {
124
+ /** Explicit provider-hosted web search support; absent means unsupported. */
125
+ supportsHostedWebSearch?: boolean
124
126
  /** Native JSON Schema response-format mapping. Absent means unsupported. */
125
127
  supportsNativeStructuredOutput?: boolean
126
128
  supportsTools: boolean
@@ -9,6 +9,13 @@ export interface StreamChunk {
9
9
  */
10
10
  replayState?: unknown
11
11
  delta: {
12
+ /** Activity executed by the provider, never a local function-call request. */
13
+ hostedTool?: {
14
+ id: string
15
+ name: 'web_search'
16
+ status: 'running' | 'completed' | 'failed'
17
+ }
18
+
12
19
  content?: string
13
20
  toolCalls?: Array<{
14
21
  index: number
@@ -65,6 +65,8 @@ export interface AgentRunConfig {
65
65
  * high-effort and silently was not is worse than a startup error.
66
66
  */
67
67
  effort?: import('../provider/index.js').ReasoningEffort
68
+ /** Provider-hosted search for this run. Explicit opt-in; no local network permission. */
69
+ webSearch?: import('../provider/index.js').ChatCompletionParams['webSearch']
68
70
  tokenBudget: number
69
71
  costLimitUsd?: number
70
72
  maxIterations?: number
@@ -17,6 +17,7 @@ import type { Message } from '../message/index.js'
17
17
  import type { PlanStep } from '../plan/index.js'
18
18
  import type { PluginHookEvent, PluginHookResult } from '../plugin/index.js'
19
19
  import type { TaskStatus } from '../task/index.js'
20
+ import type { ToolResultView } from '../tool/presentation.js'
20
21
  import type { CancelCause } from './cancel-cause.js'
21
22
  import type { FencingToken } from './checkpoint-store.js'
22
23
  import type { Lineage } from './lineage.js'
@@ -367,6 +368,13 @@ type CoreRunEvent =
367
368
  /** Optional completion in [0,1] when the tool genuinely knows it. */
368
369
  fraction?: number
369
370
  }
371
+ /** Provider-executed activity, retained as evidence rather than a local tool request. */
372
+ | {
373
+ type: 'hosted_tool'
374
+ runId: RunId
375
+ iteration: number
376
+ tool: NonNullable<import('../provider/index.js').StreamChunk['delta']['hostedTool']>
377
+ }
370
378
  /**
371
379
  * A model call failed transiently and is being retried after a backoff.
372
380
  *
@@ -426,6 +434,8 @@ type CoreRunEvent =
426
434
  }
427
435
  | {
428
436
  type: 'tool_completed'
437
+ /** Bounded result view; omitted when output was overridden or truncated. */
438
+ presentation?: ToolResultView
429
439
  runId: RunId
430
440
  toolUseId: ToolUseId
431
441
  toolName: string
@@ -310,6 +310,7 @@ export interface Sandbox {
310
310
  listFiles(rootPath: string): Promise<readonly SandboxFileEntry[]>
311
311
  /**
312
312
  * Lazily enumerate regular files as absolute paths, without following symlinks.
313
+ * A regular-file root yields that file if its basename matches the pattern.
313
314
  * Cancellation and iterator return stop traversal. Exceeding the examined-entry
314
315
  * budget throws an error with code `ERR_FILE_WALK_LIMIT`; it is not an empty or
315
316
  * complete listing. Hosts requiring bounded search must refuse an absent method.
@@ -22,6 +22,8 @@ export type ToolCallView =
22
22
  readonly label: string
23
23
  /** Render this complete authored label without adding the registry name. */
24
24
  readonly presentation?: 'activity'
25
+ /** Successful observations may be grouped by hosts without discarding output. */
26
+ readonly activity?: 'exploration'
25
27
  /** A successful result may add no information beyond the completed call row. */
26
28
  readonly visibility?: 'hidden'
27
29
  }
@@ -32,6 +34,8 @@ export type ToolCallView =
32
34
  */
33
35
  | {
34
36
  readonly kind: 'diff'
37
+ /** Optional operation summary, distinct from the file path. */
38
+ readonly label?: string
35
39
  readonly path?: string
36
40
  readonly before: string
37
41
  readonly after: string