@namzu/sdk 52.0.0 → 52.2.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 (157) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/compaction/verifier.d.ts.map +1 -1
  4. package/dist/compaction/verifier.js +57 -17
  5. package/dist/compaction/verifier.js.map +1 -1
  6. package/dist/connector/tools/adapter.d.ts +2 -2
  7. package/dist/contracts/a2a.d.ts +94 -94
  8. package/dist/manager/resident/learning.d.ts +6 -6
  9. package/dist/manager/resident/store.d.ts +4 -4
  10. package/dist/pals/activity/subscription-schema.d.ts +48 -48
  11. package/dist/pals/communication/ingress-schema.d.ts +192 -192
  12. package/dist/pals/communication/schema.d.ts +18 -18
  13. package/dist/peers/protocol.d.ts +36 -36
  14. package/dist/provider/capabilities.d.ts +9 -0
  15. package/dist/provider/capabilities.d.ts.map +1 -1
  16. package/dist/provider/capabilities.js +15 -0
  17. package/dist/provider/capabilities.js.map +1 -1
  18. package/dist/provider/errors.d.ts.map +1 -1
  19. package/dist/provider/errors.js +13 -0
  20. package/dist/provider/errors.js.map +1 -1
  21. package/dist/provider/fallback.d.ts.map +1 -1
  22. package/dist/provider/fallback.js +10 -0
  23. package/dist/provider/fallback.js.map +1 -1
  24. package/dist/provider/idle-timeout.d.ts.map +1 -1
  25. package/dist/provider/idle-timeout.js +5 -0
  26. package/dist/provider/idle-timeout.js.map +1 -1
  27. package/dist/provider/image-input-refusal.d.ts +9 -0
  28. package/dist/provider/image-input-refusal.d.ts.map +1 -0
  29. package/dist/provider/image-input-refusal.js +54 -0
  30. package/dist/provider/image-input-refusal.js.map +1 -0
  31. package/dist/provider/index.d.ts +2 -1
  32. package/dist/provider/index.d.ts.map +1 -1
  33. package/dist/provider/index.js +2 -1
  34. package/dist/provider/index.js.map +1 -1
  35. package/dist/provider/retry.d.ts +7 -2
  36. package/dist/provider/retry.d.ts.map +1 -1
  37. package/dist/provider/retry.js +38 -4
  38. package/dist/provider/retry.js.map +1 -1
  39. package/dist/public-runtime.d.ts +2 -1
  40. package/dist/public-runtime.d.ts.map +1 -1
  41. package/dist/public-runtime.js +1 -1
  42. package/dist/public-runtime.js.map +1 -1
  43. package/dist/public-tools.d.ts +1 -0
  44. package/dist/public-tools.d.ts.map +1 -1
  45. package/dist/public-tools.js +1 -0
  46. package/dist/public-tools.js.map +1 -1
  47. package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
  48. package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
  49. package/dist/runtime/query/deferred-sandbox.js +75 -0
  50. package/dist/runtime/query/deferred-sandbox.js.map +1 -0
  51. package/dist/runtime/query/executor.d.ts +11 -0
  52. package/dist/runtime/query/executor.d.ts.map +1 -1
  53. package/dist/runtime/query/executor.js +25 -3
  54. package/dist/runtime/query/executor.js.map +1 -1
  55. package/dist/runtime/query/index.d.ts +2 -0
  56. package/dist/runtime/query/index.d.ts.map +1 -1
  57. package/dist/runtime/query/index.js +94 -25
  58. package/dist/runtime/query/index.js.map +1 -1
  59. package/dist/runtime/query/iteration/index.d.ts +26 -0
  60. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/index.js +164 -21
  62. package/dist/runtime/query/iteration/index.js.map +1 -1
  63. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +10 -7
  64. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  65. package/dist/runtime/query/iteration/phases/compaction-breaker.js +10 -7
  66. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  67. package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
  68. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  69. package/dist/runtime/query/iteration/phases/compaction.js +119 -21
  70. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  71. package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
  72. package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
  73. package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
  74. package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
  75. package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
  76. package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/request-repair.js +593 -0
  78. package/dist/runtime/query/iteration/request-repair.js.map +1 -0
  79. package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
  80. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/stream-turn.js +2 -2
  82. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  83. package/dist/runtime/query/request-rich-content.d.ts +5 -1
  84. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  85. package/dist/runtime/query/request-rich-content.js +67 -25
  86. package/dist/runtime/query/request-rich-content.js.map +1 -1
  87. package/dist/runtime/query/tooling.d.ts +1 -0
  88. package/dist/runtime/query/tooling.d.ts.map +1 -1
  89. package/dist/runtime/query/tooling.js +1 -0
  90. package/dist/runtime/query/tooling.js.map +1 -1
  91. package/dist/tools/builtins/browser.d.ts +4 -4
  92. package/dist/tools/builtins/code-analysis.d.ts +49 -0
  93. package/dist/tools/builtins/code-analysis.d.ts.map +1 -0
  94. package/dist/tools/builtins/code-analysis.js +409 -0
  95. package/dist/tools/builtins/code-analysis.js.map +1 -0
  96. package/dist/tools/builtins/computer-use.d.ts +25 -25
  97. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  98. package/dist/tools/builtins/computer-use.js +12 -5
  99. package/dist/tools/builtins/computer-use.js.map +1 -1
  100. package/dist/tools/builtins/index.d.ts +1 -0
  101. package/dist/tools/builtins/index.d.ts.map +1 -1
  102. package/dist/tools/builtins/index.js +1 -0
  103. package/dist/tools/builtins/index.js.map +1 -1
  104. package/dist/tools/builtins/lsp.d.ts +1 -1
  105. package/dist/tools/builtins/skill.d.ts +2 -2
  106. package/dist/types/code-analysis/index.d.ts +299 -0
  107. package/dist/types/code-analysis/index.d.ts.map +1 -0
  108. package/dist/types/code-analysis/index.js +30 -0
  109. package/dist/types/code-analysis/index.js.map +1 -0
  110. package/dist/types/plugin/index.d.ts +6 -6
  111. package/dist/types/provider/errors.d.ts.map +1 -1
  112. package/dist/types/provider/errors.js +30 -5
  113. package/dist/types/provider/errors.js.map +1 -1
  114. package/dist/types/provider/interface.d.ts +12 -0
  115. package/dist/types/provider/interface.d.ts.map +1 -1
  116. package/dist/types/provider/stream.d.ts +1 -1
  117. package/dist/types/session/config.d.ts +14 -0
  118. package/dist/types/session/config.d.ts.map +1 -1
  119. package/dist/types/session/events.d.ts +20 -5
  120. package/dist/types/session/events.d.ts.map +1 -1
  121. package/dist/types/session/events.js.map +1 -1
  122. package/dist/types/session/records.d.ts +127 -127
  123. package/dist/types/tool/index.d.ts +9 -0
  124. package/dist/types/tool/index.d.ts.map +1 -1
  125. package/dist/types/tool/index.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/compaction/verifier.ts +57 -20
  128. package/src/provider/capabilities.ts +17 -0
  129. package/src/provider/errors.ts +13 -0
  130. package/src/provider/fallback.ts +10 -0
  131. package/src/provider/idle-timeout.ts +5 -0
  132. package/src/provider/image-input-refusal.ts +54 -0
  133. package/src/provider/index.ts +2 -0
  134. package/src/provider/retry.ts +42 -4
  135. package/src/public-runtime.ts +28 -0
  136. package/src/public-tools.ts +5 -0
  137. package/src/runtime/query/deferred-sandbox.ts +110 -0
  138. package/src/runtime/query/executor.ts +27 -3
  139. package/src/runtime/query/index.ts +127 -49
  140. package/src/runtime/query/iteration/index.ts +188 -23
  141. package/src/runtime/query/iteration/phases/compaction-breaker.ts +10 -8
  142. package/src/runtime/query/iteration/phases/compaction.ts +151 -35
  143. package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
  144. package/src/runtime/query/iteration/request-repair.ts +743 -0
  145. package/src/runtime/query/iteration/stream-turn.ts +11 -1
  146. package/src/runtime/query/request-rich-content.ts +77 -27
  147. package/src/runtime/query/tooling.ts +2 -0
  148. package/src/tools/builtins/code-analysis.ts +486 -0
  149. package/src/tools/builtins/computer-use.ts +13 -4
  150. package/src/tools/builtins/index.ts +5 -0
  151. package/src/types/code-analysis/index.ts +325 -0
  152. package/src/types/provider/errors.ts +33 -5
  153. package/src/types/provider/interface.ts +13 -0
  154. package/src/types/provider/stream.ts +1 -1
  155. package/src/types/session/config.ts +14 -0
  156. package/src/types/session/events.ts +25 -5
  157. package/src/types/tool/index.ts +9 -0
@@ -0,0 +1,486 @@
1
+ import { realpath, stat } from 'node:fs/promises'
2
+ import { z } from 'zod'
3
+
4
+ import type {
5
+ AnalysisEvidence,
6
+ AnalysisFunction,
7
+ AnalysisModuleEdge,
8
+ AnalysisPage,
9
+ AnalysisSourceSlice,
10
+ CodeAnalysisProvider,
11
+ CodeAnalysisResult,
12
+ } from '../../types/code-analysis/index.js'
13
+ import type { ToolResult } from '../../types/tool/index.js'
14
+ import { defineTool } from '../defineTool.js'
15
+ import { isWithin, resolveWithinAnyReal, toolRoots } from '../paths.js'
16
+
17
+ /**
18
+ * Cited lookups over a semantic index of JavaScript and TypeScript.
19
+ *
20
+ * For the question `grep` answers with a line and nothing around it: *this
21
+ * string is in the code — which function is it in, what is that function's
22
+ * whole source, and what calls it?* It is built for code nobody wrote for you
23
+ * to read (a minified bundle, a vendored build), where the function has no
24
+ * useful name and the line is 40,000 characters wide.
25
+ *
26
+ * **Not in `getBuiltinTools()`, and not registered when no provider is
27
+ * configured**, for the reason `lsp` is not: a tool that is always present
28
+ * and always answers "unavailable" costs a decision on every turn to say
29
+ * nothing. See `tools/builtins/lsp.ts`.
30
+ *
31
+ * ## One path argument, one boundary
32
+ *
33
+ * Every operation takes `path`, declared as this tool's `pathArgument`, and
34
+ * resolves it with `resolveWithinAnyReal` over `toolRoots` — the resolver and
35
+ * the review route `read` and `grep` use. A path outside the roots is
36
+ * reviewed before the call runs when the host reviews such paths, and refused
37
+ * with the same message otherwise. There is no second way past the boundary:
38
+ * the index is built from files under a directory THIS tool has just
39
+ * resolved, never from a path the model supplied to the provider directly.
40
+ */
41
+
42
+ /** Items per page when the call names none. */
43
+ const DEFAULT_LIMIT = 50
44
+ /** The most a page may hold, whatever the call asks for. */
45
+ const MAX_LIMIT = 200
46
+ /** Source characters shown when the call names no cap. */
47
+ const DEFAULT_MAX_CHARS = 20_000
48
+ const MAX_MAX_CHARS = 200_000
49
+
50
+ const paging = {
51
+ limit: z
52
+ .number()
53
+ .int()
54
+ .min(1)
55
+ .max(MAX_LIMIT)
56
+ .optional()
57
+ .describe(`Results per page (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}).`),
58
+ cursor: z.string().optional().describe('`next cursor` from the previous page, unchanged.'),
59
+ }
60
+
61
+ const maxChars = z
62
+ .number()
63
+ .int()
64
+ .min(200)
65
+ .max(MAX_MAX_CHARS)
66
+ .optional()
67
+ .describe(`Cap on source shown (default ${DEFAULT_MAX_CHARS}); a cut is reported.`)
68
+
69
+ // Descriptions are short on purpose: this schema is sent with every request,
70
+ // and the first draft of it cost four times what `grep`'s does.
71
+ const indexOp = z.object({
72
+ operation: z
73
+ .literal('index')
74
+ .describe('Build or refresh the index (incremental). Other operations build it on first use.'),
75
+ path: z.string().optional().describe('A session directory; defaults to the working directory.'),
76
+ force: z.boolean().optional().describe('Re-read every file.'),
77
+ })
78
+
79
+ const findLiteralOp = z.object({
80
+ operation: z
81
+ .literal('find_literal')
82
+ .describe('Find string/template literals containing `text`, each with its enclosing function.'),
83
+ text: z.string().min(1),
84
+ path: z
85
+ .string()
86
+ .optional()
87
+ .describe('File or directory to search; defaults to the working directory.'),
88
+ case_sensitive: z.boolean().optional().describe('Default true.'),
89
+ ...paging,
90
+ })
91
+
92
+ const enclosingOp = z.object({
93
+ operation: z
94
+ .literal('enclosing_function')
95
+ .describe('The function containing a position: exact source, line range, parent chain.'),
96
+ path: z.string(),
97
+ line: z.number().int().min(1).describe('1-based.'),
98
+ column: z
99
+ .number()
100
+ .int()
101
+ .min(1)
102
+ .optional()
103
+ .describe('1-based UTF-16 column (default 1); needed on minified lines.'),
104
+ max_chars: maxChars,
105
+ })
106
+
107
+ const functionSourceOp = z.object({
108
+ operation: z
109
+ .literal('function_source')
110
+ .describe(
111
+ "A function's exact source, by `function_id` from a result, or by `path` and `name`.",
112
+ ),
113
+ function_id: z.number().int().min(0).optional(),
114
+ path: z.string().optional(),
115
+ name: z.string().min(1).optional(),
116
+ max_chars: maxChars,
117
+ ...paging,
118
+ })
119
+
120
+ const callersOp = z.object({
121
+ operation: z
122
+ .literal('callers_of')
123
+ .describe(
124
+ 'Call sites of a callee TEXT (not scope-aware), each with its caller. Per file by default.',
125
+ ),
126
+ name: z.string().min(1).describe('Callee text: `foo`, `obj.foo`, `this.foo`.'),
127
+ scope: z
128
+ .enum(['file', 'workspace'])
129
+ .optional()
130
+ .describe('`file` (default) needs `path`; `workspace` mixes same-named functions.'),
131
+ path: z.string().optional(),
132
+ match: z.enum(['exact', 'member']).optional().describe('`member` also matches `<expr>.name`.'),
133
+ ...paging,
134
+ })
135
+
136
+ const importsOp = z.object({
137
+ operation: z.literal('imports').describe("One file's imports and exports."),
138
+ path: z.string(),
139
+ ...paging,
140
+ })
141
+
142
+ const inputSchema = z.discriminatedUnion('operation', [
143
+ indexOp,
144
+ findLiteralOp,
145
+ enclosingOp,
146
+ functionSourceOp,
147
+ callersOp,
148
+ importsOp,
149
+ ])
150
+
151
+ type CodeAnalysisInput = z.infer<typeof inputSchema>
152
+
153
+ export const CODE_ANALYSIS_TOOL_NAME = 'code_analysis'
154
+
155
+ export const CodeAnalysisTool = defineTool({
156
+ name: CODE_ANALYSIS_TOOL_NAME,
157
+ // The first sentence is the discovery hint a deferred listing shows (about
158
+ // 100 characters), so it carries the two reasons to reach for this tool.
159
+ description:
160
+ "Find which function holds a string in minified or unfamiliar JS/TS bundles; read it, list callers. Paged and cited as path:line:column; each result says what was observed, what was inferred and whether coverage is complete. Also lists a file's imports.",
161
+ inputSchema,
162
+ category: 'filesystem',
163
+ // Declared so a path outside the turn's roots can be reviewed before the
164
+ // call runs, rather than refused after. See `ToolDefinition.pathArgument`.
165
+ pathArgument: 'path',
166
+ permissions: ['file_read'],
167
+ // Reads source files and writes only its own cache, outside the roots.
168
+ readOnly: true,
169
+ destructive: false,
170
+ concurrencySafe: true,
171
+
172
+ async execute(input: CodeAnalysisInput, context): Promise<ToolResult> {
173
+ const provider = context.codeAnalysis
174
+ if (!provider) {
175
+ return fail(
176
+ 'This turn has no code analysis provider, so there is no index to query. A host supplies one on the tool context; `grep` is the fallback, and it answers a different question.',
177
+ )
178
+ }
179
+ if (context.sandbox) {
180
+ // The index reads the HOST file system. In a sandboxed turn the
181
+ // workspace the model sees is not that one, and answering from the
182
+ // host would cite files the turn cannot open.
183
+ return fail(
184
+ 'This turn runs in a sandbox, and the code index reads the host file system, not the sandbox workspace. Use `grep` and `read` here.',
185
+ )
186
+ }
187
+
188
+ const roots = toolRoots(context)
189
+ let target: string
190
+ try {
191
+ // The SAME resolver `read` runs, so a path is on one side of the
192
+ // boundary for both tools or for neither.
193
+ target = await resolveWithinAnyReal(roots, input.path)
194
+ } catch (err) {
195
+ return fail(err instanceof Error ? err.message : String(err))
196
+ }
197
+
198
+ // `function_id` alone names no path; the root is then the working one.
199
+ const owner = await owningRoot(roots, target)
200
+ if (owner.kind === 'file') {
201
+ return fail(
202
+ `${target} was reached as a single file, not through a directory this turn may read, and the index is built over a directory. Ask the user to add its directory to the session.`,
203
+ )
204
+ }
205
+ if (owner.kind === 'none') {
206
+ return fail(`No readable directory of this turn contains ${target}.`)
207
+ }
208
+ const root = owner.root
209
+
210
+ switch (input.operation) {
211
+ case 'index': {
212
+ const result = await provider.index({
213
+ root,
214
+ // A narrower path indexes just that part of the root, which is
215
+ // how a root too big to index whole is covered in steps.
216
+ ...(target !== root ? { scope: target } : {}),
217
+ ...(input.force ? { force: true } : {}),
218
+ ...signalOf(context),
219
+ })
220
+ return render(result, (value) => {
221
+ const f = value.files
222
+ const lines = [
223
+ `Indexed ${value.root}: ${f.indexed} files (${f.parsed} parsed, ${f.unchanged} unchanged, ${f.removed} removed, ${f.skipped} skipped, ${f.withParseErrors} with parse errors) in ${value.durationMs} ms.`,
224
+ `${value.functions} functions, ${value.strings} string literals, ${value.calls} calls. ${value.persisted ? 'Cached on disk.' : 'Held in memory only; it is gone when this process exits.'}`,
225
+ ]
226
+ if (value.truncated) lines.push(value.truncated)
227
+ const reasons = Object.entries(value.skippedReasons)
228
+ if (reasons.length > 0) {
229
+ lines.push(`Skipped: ${reasons.map(([why, n]) => `${n} ${why}`).join(', ')}.`)
230
+ }
231
+ return lines
232
+ })
233
+ }
234
+ case 'find_literal': {
235
+ const result = await provider.findLiteral({
236
+ root,
237
+ text: input.text,
238
+ path: target,
239
+ ...(input.case_sensitive === false ? { caseSensitive: false } : {}),
240
+ ...pageArgs(input),
241
+ ...signalOf(context),
242
+ })
243
+ return renderPage(result, (hit) => {
244
+ const where = `${hit.path}:${hit.line}:${hit.column}`
245
+ const shown = JSON.stringify(hit.value) + (hit.valueTruncated ? ' (cut)' : '')
246
+ const inside = hit.function ? ` in ${describeFunction(hit.function)}` : ' at top level'
247
+ return `${where} ${hit.kind} ${shown}${inside}${staleMark(hit.stale)}`
248
+ })
249
+ }
250
+ case 'enclosing_function': {
251
+ const result = await provider.enclosingFunction({
252
+ root,
253
+ path: target,
254
+ line: input.line,
255
+ ...(input.column !== undefined ? { column: input.column } : {}),
256
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
257
+ ...signalOf(context),
258
+ })
259
+ return render(result, (value) => {
260
+ if (!value.function) {
261
+ return [`${target}:${input.line}:${input.column ?? 1} is not inside any function.`]
262
+ }
263
+ const lines = [`${describeFunction(value.function)}`]
264
+ if (value.parents.length > 0) {
265
+ lines.push(`parents: ${value.parents.map(describeFunction).join(' < ')}`)
266
+ }
267
+ if (value.source) lines.push(...sourceLines(value.source))
268
+ return lines
269
+ })
270
+ }
271
+ case 'function_source': {
272
+ if (input.function_id === undefined && !(input.path !== undefined && input.name)) {
273
+ return fail('function_source needs `function_id`, or `path` together with `name`.')
274
+ }
275
+ const result = await provider.functionSource({
276
+ root,
277
+ ...(input.function_id !== undefined ? { id: input.function_id } : {}),
278
+ ...(input.name ? { name: input.name, path: target } : {}),
279
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
280
+ ...pageArgs(input),
281
+ ...signalOf(context),
282
+ })
283
+ return renderPage(result, (item) =>
284
+ [
285
+ describeFunction(item.function),
286
+ item.parents.length > 0
287
+ ? `parents: ${item.parents.map(describeFunction).join(' < ')}`
288
+ : undefined,
289
+ ...sourceLines(item.source),
290
+ ]
291
+ .filter((l): l is string => l !== undefined)
292
+ .join('\n'),
293
+ )
294
+ }
295
+ case 'callers_of': {
296
+ const scope = input.scope ?? 'file'
297
+ if (scope === 'file' && input.path === undefined) {
298
+ return fail(
299
+ 'callers_of looks in one file by default, so it needs `path`. Pass `scope: "workspace"` to search the whole index; it matches by name only and will mix unrelated functions that share one.',
300
+ )
301
+ }
302
+ const result = await provider.callersOf({
303
+ root,
304
+ name: input.name,
305
+ scope,
306
+ ...(input.path !== undefined ? { path: target } : {}),
307
+ ...(input.match ? { match: input.match } : {}),
308
+ ...pageArgs(input),
309
+ ...signalOf(context),
310
+ })
311
+ return renderPage(result, (site) => {
312
+ const inside = site.caller ? ` in ${describeFunction(site.caller)}` : ' at top level'
313
+ return `${site.path}:${site.line}:${site.column} ${site.kind === 'new' ? 'new ' : ''}${site.callee}(${site.argumentCount} arg${site.argumentCount === 1 ? '' : 's'})${inside}${staleMark(site.stale)}`
314
+ })
315
+ }
316
+ case 'imports': {
317
+ const result = await provider.imports({
318
+ root,
319
+ path: target,
320
+ ...pageArgs(input),
321
+ ...signalOf(context),
322
+ })
323
+ return renderPage(result, (edge) => describeEdge(edge, target))
324
+ }
325
+ }
326
+ },
327
+ })
328
+
329
+ function signalOf(context: { abortSignal?: AbortSignal }): { signal?: AbortSignal } {
330
+ return context.abortSignal ? { signal: context.abortSignal } : {}
331
+ }
332
+
333
+ function staleMark(stale: 'changed' | 'deleted' | undefined): string {
334
+ if (stale === 'deleted') return ' [file deleted since indexed]'
335
+ if (stale === 'changed') return ' [file changed since indexed; position may be off]'
336
+ return ''
337
+ }
338
+
339
+ function fail(error: string): ToolResult {
340
+ return { success: false, output: '', error }
341
+ }
342
+
343
+ function pageArgs(input: { limit?: number | undefined; cursor?: string | undefined }): {
344
+ limit: number
345
+ cursor?: string
346
+ } {
347
+ return {
348
+ limit: input.limit ?? DEFAULT_LIMIT,
349
+ ...(input.cursor !== undefined ? { cursor: input.cursor } : {}),
350
+ }
351
+ }
352
+
353
+ type OwningRoot =
354
+ | { readonly kind: 'dir'; readonly root: string }
355
+ | { readonly kind: 'file' }
356
+ | { readonly kind: 'none' }
357
+
358
+ /**
359
+ * The directory of this turn that contains `target`, canonicalised.
360
+ *
361
+ * Roots are directories except when an approval named a single file: that
362
+ * file is a root for ONE call, and indexing its parent would read what the
363
+ * approval never covered. So a file reached only that way is refused rather
364
+ * than widened.
365
+ */
366
+ async function owningRoot(roots: readonly string[], target: string): Promise<OwningRoot> {
367
+ let sawFile = false
368
+ for (const candidate of roots) {
369
+ let real: string
370
+ try {
371
+ real = await realpath(candidate)
372
+ if (!(await stat(real)).isDirectory()) {
373
+ if (isWithin(real, target) || real === target) sawFile = true
374
+ continue
375
+ }
376
+ } catch {
377
+ continue
378
+ }
379
+ if (isWithin(real, target)) return { kind: 'dir', root: real }
380
+ }
381
+ return sawFile ? { kind: 'file' } : { kind: 'none' }
382
+ }
383
+
384
+ function describeFunction(fn: AnalysisFunction): string {
385
+ const lines = fn.endLine === fn.line ? `line ${fn.line}` : `lines ${fn.line}-${fn.endLine}`
386
+ return `${fn.name ?? '(anonymous)'} [${fn.kind}, id ${fn.id}] ${fn.path}:${fn.line}:${fn.column} (${lines})`
387
+ }
388
+
389
+ function describeEdge(edge: AnalysisModuleEdge, path: string): string {
390
+ const where = `${path}:${edge.line}:${edge.column}`
391
+ if (edge.direction === 'import') {
392
+ const names = edge.names.length > 0 ? ` { ${edge.names.join(', ')} }` : ''
393
+ return `${where} import (${edge.kind})${names} from ${JSON.stringify(edge.source)}`
394
+ }
395
+ const from = edge.source !== undefined ? ` from ${JSON.stringify(edge.source)}` : ''
396
+ return `${where} export (${edge.kind}) ${edge.name}${from}`
397
+ }
398
+
399
+ function sourceLines(slice: AnalysisSourceSlice): string[] {
400
+ const lines = ['--- source ---', slice.text]
401
+ if (slice.truncated) {
402
+ lines.push(
403
+ `--- cut: showing ${slice.text.length} of ${slice.chars} characters; raise max_chars to see more ---`,
404
+ )
405
+ }
406
+ return lines
407
+ }
408
+
409
+ function evidenceLines(evidence: AnalysisEvidence): string[] {
410
+ const lines: string[] = []
411
+ if (evidence.observed.length > 0) lines.push(`observed: ${evidence.observed.join('; ')}`)
412
+ if (evidence.inferred.length > 0) lines.push(`inferred: ${evidence.inferred.join('; ')}`)
413
+ lines.push(
414
+ evidence.coverage.status === 'complete'
415
+ ? 'coverage: complete'
416
+ : `coverage: partial - ${evidence.coverage.reasons.join('; ')}`,
417
+ )
418
+ return lines
419
+ }
420
+
421
+ function render<T>(
422
+ result: CodeAnalysisResult<T>,
423
+ body: (value: T) => string[],
424
+ data?: (value: T) => unknown,
425
+ ): ToolResult {
426
+ switch (result.kind) {
427
+ case 'ok':
428
+ return {
429
+ success: true,
430
+ output: [...body(result.value), ...evidenceLines(result.evidence)].join('\n'),
431
+ data: {
432
+ value: data ? data(result.value) : result.value,
433
+ evidence: result.evidence,
434
+ },
435
+ }
436
+ case 'unsupported':
437
+ // Distinct from a failure so the model can fall back and know why.
438
+ return fail(
439
+ `${result.reason} Fall back to \`grep\`, and treat the result as textual rather than resolved.`,
440
+ )
441
+ case 'failed':
442
+ // NOT an empty result. An agent told a name has no callers deletes it.
443
+ return fail(
444
+ `Code analysis failed, so this answer is unknown rather than empty: ${result.error}`,
445
+ )
446
+ }
447
+ }
448
+
449
+ function renderPage<T>(
450
+ result: CodeAnalysisResult<AnalysisPage<T>>,
451
+ line: (item: T) => string,
452
+ ): ToolResult {
453
+ return render(
454
+ result,
455
+ (page) => {
456
+ if (page.items.length === 0) {
457
+ return [page.total === 0 ? 'No results.' : 'No results on this page.']
458
+ }
459
+ const lines = page.items.map(line)
460
+ lines.push(
461
+ page.nextCursor !== undefined
462
+ ? `-- ${page.items.length} of ${page.total} shown. next cursor: ${page.nextCursor}`
463
+ : `-- ${page.items.length} of ${page.total} shown. last page.`,
464
+ )
465
+ return lines
466
+ },
467
+ (page) => ({
468
+ total: page.total,
469
+ count: page.items.length,
470
+ items: page.items,
471
+ ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),
472
+ }),
473
+ )
474
+ }
475
+
476
+ /**
477
+ * The tool, only when there is something for it to call.
478
+ *
479
+ * A function rather than a constant because the decision is per turn: the
480
+ * same process can serve one session with an index and one without.
481
+ */
482
+ export function getCodeAnalysisTools(
483
+ provider: CodeAnalysisProvider | undefined,
484
+ ): (typeof CodeAnalysisTool)[] {
485
+ return provider ? [CodeAnalysisTool] : []
486
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { resolveProviderCapabilities } from '../../provider/capabilities.js'
2
+ import { modelReadsImages, resolveProviderCapabilities } from '../../provider/capabilities.js'
3
3
  import type {
4
4
  ComputerUseAction,
5
5
  ComputerUseCapabilities,
@@ -105,10 +105,19 @@ export interface ComputerUseToolOptions {
105
105
  * {@link ComputerUseToolOptions.unavailableReason}.
106
106
  */
107
107
  export function computerUseUnavailableReason(
108
- provider: Pick<LLMProvider, 'id' | 'capabilities'>,
108
+ provider: Pick<LLMProvider, 'id' | 'capabilities' | 'supportsImageInputFor'>,
109
+ model?: string,
109
110
  ): string | undefined {
110
- if (resolveProviderCapabilities(provider).supportsToolResultImages) return undefined
111
- return `The ${provider.id} provider cannot return images in tool results, so the model would never see a screenshot. Use a provider that can (for example Anthropic, Codex or Google) for computer use.`
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 (for example Anthropic, Codex or Google) for computer use.`
113
+ }
114
+ // The driver can carry the image; the model behind it may still not read one.
115
+ // It would act on a screen it never saw, and the first screenshot would make
116
+ // the provider refuse every later request in the conversation.
117
+ if (model !== undefined && !modelReadsImages(provider, model)) {
118
+ return `The model ${model} cannot read images, so it would never see a screenshot. Use a model that reads images for computer use.`
119
+ }
120
+ return undefined
112
121
  }
113
122
 
114
123
  // ---------------------------------------------------------------------------
@@ -3,6 +3,11 @@ export { WriteFileTool } from './write-file.js'
3
3
  export { EditTool } from './edit.js'
4
4
  export { BashTool } from './bash.js'
5
5
  export { GlobTool } from './glob.js'
6
+ export {
7
+ CODE_ANALYSIS_TOOL_NAME,
8
+ CodeAnalysisTool,
9
+ getCodeAnalysisTools,
10
+ } from './code-analysis.js'
6
11
  export { LSP_TOOL_NAME, LspTool, getCodeNavigationTools } from './lsp.js'
7
12
  export { GrepTool } from './grep.js'
8
13
  export { JobTool } from './job.js'