@namzu/sdk 57.0.0 → 58.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 (278) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +3 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/file-definitions.d.ts +35 -4
  6. package/dist/agents/file-definitions.d.ts.map +1 -1
  7. package/dist/agents/file-definitions.js +98 -8
  8. package/dist/agents/file-definitions.js.map +1 -1
  9. package/dist/authorization/protected-paths.d.ts +9 -0
  10. package/dist/authorization/protected-paths.d.ts.map +1 -1
  11. package/dist/authorization/protected-paths.js +29 -3
  12. package/dist/authorization/protected-paths.js.map +1 -1
  13. package/dist/compaction/index.d.ts +1 -0
  14. package/dist/compaction/index.d.ts.map +1 -1
  15. package/dist/compaction/index.js.map +1 -1
  16. package/dist/compaction/manual.d.ts +7 -0
  17. package/dist/compaction/manual.d.ts.map +1 -1
  18. package/dist/compaction/manual.js +31 -12
  19. package/dist/compaction/manual.js.map +1 -1
  20. package/dist/compaction/model-summary.d.ts +101 -0
  21. package/dist/compaction/model-summary.d.ts.map +1 -0
  22. package/dist/compaction/model-summary.js +333 -0
  23. package/dist/compaction/model-summary.js.map +1 -0
  24. package/dist/compaction/verifier.d.ts +6 -1
  25. package/dist/compaction/verifier.d.ts.map +1 -1
  26. package/dist/compaction/verifier.js +3 -3
  27. package/dist/compaction/verifier.js.map +1 -1
  28. package/dist/config/runtime.d.ts +62 -0
  29. package/dist/config/runtime.d.ts.map +1 -1
  30. package/dist/config/runtime.js +16 -0
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/connector/mcp/client.d.ts +8 -0
  33. package/dist/connector/mcp/client.d.ts.map +1 -1
  34. package/dist/connector/mcp/client.js +75 -2
  35. package/dist/connector/mcp/client.js.map +1 -1
  36. package/dist/constants/mcp/index.d.ts +2 -0
  37. package/dist/constants/mcp/index.d.ts.map +1 -1
  38. package/dist/constants/mcp/index.js +2 -0
  39. package/dist/constants/mcp/index.js.map +1 -1
  40. package/dist/constants/plugin/index.d.ts +2 -0
  41. package/dist/constants/plugin/index.d.ts.map +1 -1
  42. package/dist/constants/plugin/index.js +2 -0
  43. package/dist/constants/plugin/index.js.map +1 -1
  44. package/dist/manager/agent/lifecycle.d.ts +1 -1
  45. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  46. package/dist/manager/agent/lifecycle.js +25 -4
  47. package/dist/manager/agent/lifecycle.js.map +1 -1
  48. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  49. package/dist/manager/session/turn-recorder.js +2 -1
  50. package/dist/manager/session/turn-recorder.js.map +1 -1
  51. package/dist/persona/assembler.d.ts.map +1 -1
  52. package/dist/persona/assembler.js +2 -2
  53. package/dist/persona/assembler.js.map +1 -1
  54. package/dist/plugin/index.d.ts +1 -1
  55. package/dist/plugin/index.d.ts.map +1 -1
  56. package/dist/plugin/lifecycle.d.ts +8 -1
  57. package/dist/plugin/lifecycle.d.ts.map +1 -1
  58. package/dist/plugin/lifecycle.js +9 -2
  59. package/dist/plugin/lifecycle.js.map +1 -1
  60. package/dist/plugin/shell-hook-http.d.ts +39 -0
  61. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  62. package/dist/plugin/shell-hook-http.js +163 -0
  63. package/dist/plugin/shell-hook-http.js.map +1 -0
  64. package/dist/plugin/shell-hook.d.ts +93 -11
  65. package/dist/plugin/shell-hook.d.ts.map +1 -1
  66. package/dist/plugin/shell-hook.js +223 -39
  67. package/dist/plugin/shell-hook.js.map +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  69. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  70. package/dist/prompt/coding-agent-doctrine.js +2 -1
  71. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -2
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +2 -2
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +1 -0
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +2 -0
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +1 -1
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/executor.d.ts +4 -0
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +47 -9
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +29 -1
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +6 -2
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  94. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  96. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/phases/compaction.js +52 -5
  99. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  101. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  105. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  106. package/dist/runtime/query/path-escalation.d.ts +2 -0
  107. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  108. package/dist/runtime/query/path-escalation.js +7 -4
  109. package/dist/runtime/query/path-escalation.js.map +1 -1
  110. package/dist/runtime/query/plugin-hooks.js +2 -2
  111. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  112. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  113. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  114. package/dist/runtime/query/prepare-turn.js +25 -0
  115. package/dist/runtime/query/prepare-turn.js.map +1 -1
  116. package/dist/runtime/query/review-policy.d.ts +3 -2
  117. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  118. package/dist/runtime/query/review-policy.js +32 -13
  119. package/dist/runtime/query/review-policy.js.map +1 -1
  120. package/dist/scheduler/local.js +1 -1
  121. package/dist/scheduler/local.js.map +1 -1
  122. package/dist/session/retention/archive.js +1 -1
  123. package/dist/session/retention/archive.js.map +1 -1
  124. package/dist/session/workspace/driver.d.ts +6 -0
  125. package/dist/session/workspace/driver.d.ts.map +1 -1
  126. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  127. package/dist/session/workspace/git-worktree.js +28 -0
  128. package/dist/session/workspace/git-worktree.js.map +1 -1
  129. package/dist/skills/index.d.ts +1 -1
  130. package/dist/skills/index.d.ts.map +1 -1
  131. package/dist/skills/index.js +1 -1
  132. package/dist/skills/index.js.map +1 -1
  133. package/dist/skills/loader.d.ts +7 -0
  134. package/dist/skills/loader.d.ts.map +1 -1
  135. package/dist/skills/loader.js +53 -0
  136. package/dist/skills/loader.js.map +1 -1
  137. package/dist/skills/registry.d.ts +1 -1
  138. package/dist/skills/registry.d.ts.map +1 -1
  139. package/dist/skills/registry.js +2 -1
  140. package/dist/skills/registry.js.map +1 -1
  141. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  142. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  143. package/dist/tools/builtins/bash-cwd.js +127 -0
  144. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  145. package/dist/tools/builtins/bash.d.ts.map +1 -1
  146. package/dist/tools/builtins/bash.js +41 -6
  147. package/dist/tools/builtins/bash.js.map +1 -1
  148. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  149. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  150. package/dist/tools/builtins/bounded-file-read.js +67 -0
  151. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  152. package/dist/tools/builtins/grep.d.ts +6 -1
  153. package/dist/tools/builtins/grep.d.ts.map +1 -1
  154. package/dist/tools/builtins/grep.js +187 -24
  155. package/dist/tools/builtins/grep.js.map +1 -1
  156. package/dist/tools/builtins/read-file.d.ts +1 -0
  157. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  158. package/dist/tools/builtins/read-file.js +113 -28
  159. package/dist/tools/builtins/read-file.js.map +1 -1
  160. package/dist/tools/builtins/read-media.d.ts +68 -0
  161. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  162. package/dist/tools/builtins/read-media.js +397 -0
  163. package/dist/tools/builtins/read-media.js.map +1 -0
  164. package/dist/tools/builtins/search-tools.js +1 -1
  165. package/dist/tools/builtins/search-tools.js.map +1 -1
  166. package/dist/tools/builtins/skill.d.ts +4 -2
  167. package/dist/tools/builtins/skill.d.ts.map +1 -1
  168. package/dist/tools/builtins/skill.js +61 -2
  169. package/dist/tools/builtins/skill.js.map +1 -1
  170. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  171. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  172. package/dist/tools/builtins/sleep-guard.js +33 -0
  173. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  174. package/dist/tools/builtins/view-image.d.ts +23 -1
  175. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  176. package/dist/tools/builtins/view-image.js +66 -58
  177. package/dist/tools/builtins/view-image.js.map +1 -1
  178. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  179. package/dist/tools/file-read-tracker.js +31 -0
  180. package/dist/tools/file-read-tracker.js.map +1 -1
  181. package/dist/toolsets/manager.js +3 -3
  182. package/dist/toolsets/manager.js.map +1 -1
  183. package/dist/types/agent/base.d.ts +8 -0
  184. package/dist/types/agent/base.d.ts.map +1 -1
  185. package/dist/types/agent/query.d.ts +2 -0
  186. package/dist/types/agent/query.d.ts.map +1 -1
  187. package/dist/types/agent/scheduler.d.ts +8 -0
  188. package/dist/types/agent/scheduler.d.ts.map +1 -1
  189. package/dist/types/agent/task.d.ts +6 -2
  190. package/dist/types/agent/task.d.ts.map +1 -1
  191. package/dist/types/agent/task.js.map +1 -1
  192. package/dist/types/connector/mcp.d.ts +46 -0
  193. package/dist/types/connector/mcp.d.ts.map +1 -1
  194. package/dist/types/hitl/index.d.ts +1 -1
  195. package/dist/types/plugin/index.d.ts +74 -7
  196. package/dist/types/plugin/index.d.ts.map +1 -1
  197. package/dist/types/plugin/index.js +12 -1
  198. package/dist/types/plugin/index.js.map +1 -1
  199. package/dist/types/session/sub-session.d.ts +6 -2
  200. package/dist/types/session/sub-session.d.ts.map +1 -1
  201. package/dist/types/skills/index.d.ts +36 -0
  202. package/dist/types/skills/index.d.ts.map +1 -1
  203. package/dist/types/skills/index.js +9 -0
  204. package/dist/types/skills/index.js.map +1 -1
  205. package/dist/types/tool/index.d.ts +37 -0
  206. package/dist/types/tool/index.d.ts.map +1 -1
  207. package/dist/types/tool/index.js.map +1 -1
  208. package/dist/types/workspace/ref.d.ts +2 -0
  209. package/dist/types/workspace/ref.d.ts.map +1 -1
  210. package/dist/utils/frontmatter.d.ts +12 -0
  211. package/dist/utils/frontmatter.d.ts.map +1 -1
  212. package/dist/utils/frontmatter.js +6 -0
  213. package/dist/utils/frontmatter.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/agents/QueryAgent.ts +3 -0
  216. package/src/agents/file-definitions.ts +126 -6
  217. package/src/authorization/protected-paths.ts +30 -2
  218. package/src/compaction/index.ts +1 -0
  219. package/src/compaction/manual.ts +68 -30
  220. package/src/compaction/model-summary.ts +412 -0
  221. package/src/compaction/verifier.ts +3 -3
  222. package/src/config/runtime.ts +16 -0
  223. package/src/connector/mcp/client.ts +87 -1
  224. package/src/constants/mcp/index.ts +3 -0
  225. package/src/constants/plugin/index.ts +2 -0
  226. package/src/manager/agent/lifecycle.ts +24 -5
  227. package/src/manager/session/turn-recorder.ts +2 -1
  228. package/src/persona/assembler.ts +2 -2
  229. package/src/plugin/index.ts +5 -0
  230. package/src/plugin/lifecycle.ts +16 -2
  231. package/src/plugin/shell-hook-http.ts +181 -0
  232. package/src/plugin/shell-hook.ts +386 -89
  233. package/src/prompt/coding-agent-doctrine.ts +2 -1
  234. package/src/public-runtime.ts +6 -0
  235. package/src/public-tools.ts +2 -0
  236. package/src/public-types.ts +5 -0
  237. package/src/runtime/query/executor.ts +54 -10
  238. package/src/runtime/query/index.ts +44 -0
  239. package/src/runtime/query/iteration/index.ts +11 -3
  240. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  241. package/src/runtime/query/iteration/phases/compaction.ts +67 -5
  242. package/src/runtime/query/iteration/phases/context.ts +6 -0
  243. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  244. package/src/runtime/query/path-escalation.ts +11 -5
  245. package/src/runtime/query/plugin-hooks.ts +2 -2
  246. package/src/runtime/query/prepare-turn.ts +33 -0
  247. package/src/runtime/query/review-policy.ts +45 -16
  248. package/src/scheduler/local.ts +1 -1
  249. package/src/session/retention/archive.ts +1 -1
  250. package/src/session/workspace/driver.ts +6 -0
  251. package/src/session/workspace/git-worktree.ts +31 -0
  252. package/src/skills/index.ts +6 -1
  253. package/src/skills/loader.ts +54 -0
  254. package/src/skills/registry.ts +7 -6
  255. package/src/tools/builtins/bash-cwd.ts +156 -0
  256. package/src/tools/builtins/bash.ts +79 -34
  257. package/src/tools/builtins/bounded-file-read.ts +80 -0
  258. package/src/tools/builtins/grep.ts +198 -28
  259. package/src/tools/builtins/read-file.ts +151 -30
  260. package/src/tools/builtins/read-media.ts +483 -0
  261. package/src/tools/builtins/search-tools.ts +1 -1
  262. package/src/tools/builtins/skill.ts +91 -2
  263. package/src/tools/builtins/sleep-guard.ts +31 -0
  264. package/src/tools/builtins/view-image.ts +74 -62
  265. package/src/tools/file-read-tracker.ts +30 -0
  266. package/src/toolsets/manager.ts +3 -3
  267. package/src/types/agent/base.ts +9 -0
  268. package/src/types/agent/query.ts +2 -0
  269. package/src/types/agent/scheduler.ts +9 -0
  270. package/src/types/agent/task.ts +6 -2
  271. package/src/types/connector/mcp.ts +48 -0
  272. package/src/types/hitl/index.ts +1 -1
  273. package/src/types/plugin/index.ts +102 -16
  274. package/src/types/session/sub-session.ts +6 -2
  275. package/src/types/skills/index.ts +48 -0
  276. package/src/types/tool/index.ts +38 -0
  277. package/src/types/workspace/ref.ts +2 -0
  278. package/src/utils/frontmatter.ts +21 -0
@@ -0,0 +1,483 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { mkdtemp, readdir, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { readBoundedFile } from './bounded-file-read.js'
6
+ import { screenshotTargetSize } from './computer-use-image.js'
7
+ import { describeRaster } from './view-image.js'
8
+
9
+ /**
10
+ * What `read` hands back for the files that are not plain text: the pages of a
11
+ * PDF as pictures and the cells of a notebook with what they printed. Each
12
+ * function returns text and pictures for the model, or a plain failure message.
13
+ */
14
+
15
+ /** Most pages one read may render. */
16
+ export const PDF_MAX_PAGES_PER_READ = 20
17
+ /** A PDF with more pages than this is not read whole; the model names pages. */
18
+ export const PDF_WHOLE_READ_MAX_PAGES = 10
19
+ /** The largest PDF read; larger files are refused before any tool runs. */
20
+ export const PDF_MAX_BYTES = 100 * 1024 * 1024
21
+ /** The largest notebook read in full. */
22
+ export const NOTEBOOK_MAX_BYTES = 8 * 1024 * 1024
23
+ /** Printed output of one cell above this many characters is left out. */
24
+ const CELL_OUTPUT_MAX_CHARS = 10_000
25
+ /** Encoded images plus UTF-8 text, not decoded image bytes. */
26
+ export const MEDIA_MAX_OUTPUT_BYTES = 24 * 1024 * 1024
27
+ export const NOTEBOOK_MAX_OUTPUT_BYTES = 1024 * 1024
28
+ const PAGE_MAX_BYTES = 16 * 1024 * 1024
29
+ const PDF_TOOL_TIMEOUT_MS = 120_000
30
+ const PDF_INFO_TIMEOUT_MS = 10_000
31
+ const RENDER_DPI = '100'
32
+
33
+ export interface MediaImage {
34
+ readonly mediaType: 'image/png' | 'image/jpeg' | 'image/webp'
35
+ /** Base64 of the image bytes. */
36
+ readonly data: string
37
+ }
38
+
39
+ export type MediaPart =
40
+ | { readonly type: 'text'; readonly text: string }
41
+ | ({ readonly type: 'image' } & MediaImage)
42
+
43
+ export type MediaRead =
44
+ | { readonly ok: true; readonly parts: readonly MediaPart[]; readonly facts: MediaFacts }
45
+ | { readonly ok: false; readonly message: string }
46
+
47
+ export interface MediaFacts {
48
+ readonly pages?: number
49
+ readonly totalPages?: number
50
+ readonly cells?: number
51
+ }
52
+
53
+ /** `"3"`, `"1-5"` and `"3-"`; anything else is not one range. */
54
+ export function parsePageRange(value: string): { first: number; last: number } | null {
55
+ const match = /^(\d{1,9})(?:\s*(-)\s*(\d{1,9})?)?$/.exec(value.trim())
56
+ if (!match) return null
57
+ const first = Number(match[1])
58
+ const last = match[3] ? Number(match[3]) : match[2] ? Number.POSITIVE_INFINITY : first
59
+ if (first < 1 || last < first) return null
60
+ return { first, last }
61
+ }
62
+
63
+ export function checkPageRange(
64
+ value: string,
65
+ ): { ok: true; first: number; last: number } | { ok: false; message: string } {
66
+ const range = parsePageRange(value)
67
+ if (!range) {
68
+ return {
69
+ ok: false,
70
+ message: `Invalid pages parameter: "${value}". Give one page ("3") or one range ("1-5"), not a list. To read several pages or ranges, read each one separately. Pages are 1-indexed.`,
71
+ }
72
+ }
73
+ const count =
74
+ range.last === Number.POSITIVE_INFINITY
75
+ ? PDF_MAX_PAGES_PER_READ + 1
76
+ : range.last - range.first + 1
77
+ if (count > PDF_MAX_PAGES_PER_READ) {
78
+ return {
79
+ ok: false,
80
+ message: `Page range "${value}" exceeds maximum of ${PDF_MAX_PAGES_PER_READ} pages per request. Please use a smaller range.`,
81
+ }
82
+ }
83
+ return { ok: true, first: range.first, last: range.last }
84
+ }
85
+
86
+ interface RunResult {
87
+ readonly code: number | null
88
+ readonly stdout: string
89
+ readonly stderr: string
90
+ readonly missing: boolean
91
+ readonly timedOut: boolean
92
+ }
93
+
94
+ function run(
95
+ command: string,
96
+ args: readonly string[],
97
+ input: Buffer | undefined,
98
+ signal: AbortSignal,
99
+ timeoutMs: number,
100
+ ): Promise<RunResult> {
101
+ return new Promise((resolve) => {
102
+ const child = spawn(command, [...args], { stdio: ['pipe', 'pipe', 'pipe'], signal })
103
+ let stdout = ''
104
+ let stderr = ''
105
+ let missing = false
106
+ let timedOut = false
107
+ const timer = setTimeout(() => {
108
+ timedOut = true
109
+ child.kill('SIGKILL')
110
+ }, timeoutMs)
111
+ child.stdout.on('data', (chunk: Buffer) => {
112
+ if (stdout.length < 1_000_000)
113
+ stdout += chunk.subarray(0, 1_000_000 - stdout.length).toString('utf8')
114
+ })
115
+ child.stderr.on('data', (chunk: Buffer) => {
116
+ if (stderr.length < 1_000_000)
117
+ stderr += chunk.subarray(0, 1_000_000 - stderr.length).toString('utf8')
118
+ })
119
+ child.on('error', (error: NodeJS.ErrnoException) => {
120
+ missing = error.code === 'ENOENT'
121
+ clearTimeout(timer)
122
+ resolve({ code: null, stdout, stderr: stderr || error.message, missing, timedOut })
123
+ })
124
+ child.on('close', (code) => {
125
+ clearTimeout(timer)
126
+ resolve({ code, stdout, stderr, missing, timedOut })
127
+ })
128
+ // A tool that exits early closes its stdin; that is its answer, not ours.
129
+ child.stdin.on('error', () => {})
130
+ child.stdin.end(input)
131
+ })
132
+ }
133
+
134
+ const PDF_TOOLS_MISSING =
135
+ 'Page rendering needs pdftoppm, which is not installed. Install poppler-utils (for example `brew install poppler` or `apt-get install poppler-utils`), or extract the text with another tool.'
136
+
137
+ /** The page count, or null when it cannot be read (no pdfinfo, damaged file). */
138
+ async function pdfPageCount(bytes: Buffer, signal: AbortSignal): Promise<number | null> {
139
+ const result = await run('pdfinfo', ['-'], bytes, signal, PDF_INFO_TIMEOUT_MS)
140
+ if (result.code !== 0) return null
141
+ const match = /^Pages:\s+(\d+)/m.exec(result.stdout)
142
+ return match ? Number(match[1]) : null
143
+ }
144
+
145
+ function pdfFailure(stderr: string): string {
146
+ if (/password/i.test(stderr))
147
+ return 'PDF is password-protected. Please provide an unprotected version.'
148
+ if (/damaged|corrupt|invalid|Couldn't (?:find trailer dictionary|read xref table)/i.test(stderr))
149
+ return 'PDF file is corrupted or invalid.'
150
+ const first = stderr.split(/\r?\n/).find((line) => line.trim() !== '')
151
+ return `Could not render PDF${first ? `: ${first.trim()}` : '.'}`
152
+ }
153
+
154
+ async function renderPages(
155
+ bytes: Buffer,
156
+ first: number,
157
+ last: number | undefined,
158
+ directory: string,
159
+ signal: AbortSignal,
160
+ ): Promise<{ ok: true; names: string[] } | { ok: false; message: string; pageCount?: number }> {
161
+ const args = ['-jpeg', '-r', RENDER_DPI, '-scale-to', '4096', '-f', String(first)]
162
+ if (last !== undefined && Number.isFinite(last)) args.push('-l', String(last))
163
+ args.push('-', join(directory, 'page'))
164
+ const result = await run('pdftoppm', args, bytes, signal, PDF_TOOL_TIMEOUT_MS)
165
+ if (result.missing) return { ok: false, message: PDF_TOOLS_MISSING }
166
+ if (result.timedOut)
167
+ return { ok: false, message: 'Rendering the PDF took too long and was stopped.' }
168
+ if (result.code !== 0) {
169
+ const outside = /Wrong page range given.*last page \((\d+)\)/i.exec(result.stderr)
170
+ if (outside) {
171
+ const total = Number(outside[1])
172
+ if (total === 0)
173
+ return {
174
+ ok: false,
175
+ message: 'PDF reports 0 pages (empty page tree). The PDF may be invalid.',
176
+ }
177
+ return {
178
+ ok: false,
179
+ pageCount: total,
180
+ message: `The pages requested are outside the document (the PDF has ${total} ${total === 1 ? 'page' : 'pages'}). Use a range within 1-${total}, at most ${PDF_MAX_PAGES_PER_READ} pages per request (for example pages: "1-${Math.min(total, PDF_MAX_PAGES_PER_READ)}").`,
181
+ }
182
+ }
183
+ return { ok: false, message: pdfFailure(result.stderr) }
184
+ }
185
+ const names = (await readdir(directory)).filter((name) => name.endsWith('.jpg')).sort()
186
+ if (names.length === 0)
187
+ return { ok: false, message: 'No pages were rendered. The PDF may be invalid.' }
188
+ return { ok: true, names }
189
+ }
190
+
191
+ /** Re-render one page at the size the image budget allows. */
192
+ async function renderPageFitted(
193
+ bytes: Buffer,
194
+ page: number,
195
+ width: number,
196
+ height: number,
197
+ directory: string,
198
+ signal: AbortSignal,
199
+ ): Promise<Buffer | null> {
200
+ const base = join(directory, `fit-${page}`)
201
+ const result = await run(
202
+ 'pdftoppm',
203
+ [
204
+ '-jpeg',
205
+ '-f',
206
+ String(page),
207
+ '-l',
208
+ String(page),
209
+ '-scale-to-x',
210
+ String(width),
211
+ '-scale-to-y',
212
+ String(height),
213
+ '-singlefile',
214
+ '-',
215
+ base,
216
+ ],
217
+ bytes,
218
+ signal,
219
+ PDF_TOOL_TIMEOUT_MS,
220
+ )
221
+ if (result.code !== 0) return null
222
+ return readBoundedFile(`${base}.jpg`, PAGE_MAX_BYTES, signal).catch(() => null)
223
+ }
224
+
225
+ /**
226
+ * Render pages of a PDF as pictures. With `pages` it is exactly that range
227
+ * (at most 20); without it the whole file when it has at most 10 pages, and a
228
+ * message naming the `pages` parameter when it has more.
229
+ */
230
+ export async function readPdfPages(
231
+ bytes: Buffer,
232
+ pagesArgument: string | undefined,
233
+ label: string,
234
+ signal: AbortSignal,
235
+ ): Promise<MediaRead> {
236
+ if (bytes.length > PDF_MAX_BYTES)
237
+ return { ok: false, message: 'PDF exceeds the 100 MiB input limit.' }
238
+ let first = 1
239
+ let last: number | undefined
240
+ let total: number | null = null
241
+ if (pagesArgument !== undefined) {
242
+ const checked = checkPageRange(pagesArgument)
243
+ if (!checked.ok) return { ok: false, message: checked.message }
244
+ first = checked.first
245
+ last = checked.last
246
+ total = await pdfPageCount(bytes, signal)
247
+ } else {
248
+ total = await pdfPageCount(bytes, signal)
249
+ if (total !== null && total > PDF_WHOLE_READ_MAX_PAGES) {
250
+ return {
251
+ ok: false,
252
+ message: `This PDF has ${total} pages, which is too many to read at once. Use the pages parameter to read specific page ranges (e.g., pages: "1-5"). Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.`,
253
+ }
254
+ }
255
+ last = total === null ? PDF_WHOLE_READ_MAX_PAGES : total
256
+ }
257
+ const directory = await mkdtemp(join(tmpdir(), 'namzu-pdf-'))
258
+ try {
259
+ const rendered = await renderPages(bytes, first, last, directory, signal)
260
+ if (!rendered.ok) return { ok: false, message: rendered.message }
261
+ const parts: MediaPart[] = []
262
+ let encodedBytes = 0
263
+ const firstName = rendered.names[0] as string
264
+ const firstShown = Number(/-(\d+)\.jpg$/.exec(firstName)?.[1] ?? first)
265
+ const header = `PDF pages extracted: ${rendered.names.length} ${rendered.names.length === 1 ? 'page' : 'pages'} from ${label} (${formatSize(bytes.length)})`
266
+ const lastShown = firstShown + rendered.names.length - 1
267
+ const more =
268
+ total !== null && lastShown < total
269
+ ? `. This PDF has ${total} pages. These are pages ${firstShown}-${lastShown}. Use the pages parameter to read other pages (e.g., pages: "${lastShown + 1}-${Math.min(lastShown + rendered.names.length, total)}").`
270
+ : total === null
271
+ ? `. The page count could not be read; these are pages ${firstShown}-${lastShown}.`
272
+ : ''
273
+ if (Buffer.byteLength(header + more, 'utf8') > 64 * 1024)
274
+ return { ok: false, message: 'PDF label exceeds the output budget.' }
275
+ parts.push({ type: 'text', text: `${header}${more}` })
276
+ for (const [index, name] of rendered.names.entries()) {
277
+ const page = firstShown + index
278
+ const unusable = `[Page ${page} could not be processed as an image.]`
279
+ try {
280
+ let image = await readBoundedFile(join(directory, name), PAGE_MAX_BYTES, signal)
281
+ const raster = describeRaster(image)
282
+ const target = screenshotTargetSize(raster.width, raster.height)
283
+ if (target.width !== raster.width || target.height !== raster.height) {
284
+ const fitted = await renderPageFitted(
285
+ bytes,
286
+ page,
287
+ target.width,
288
+ target.height,
289
+ directory,
290
+ signal,
291
+ )
292
+ if (!fitted) {
293
+ parts.push({ type: 'text', text: unusable })
294
+ continue
295
+ }
296
+ image = fitted
297
+ }
298
+ const size = 4 * Math.ceil(image.length / 3)
299
+ if (encodedBytes + size > MEDIA_MAX_OUTPUT_BYTES - 128 * 1024)
300
+ return {
301
+ ok: false,
302
+ message: 'PDF pictures exceed the 24 MiB encoded output budget. Request fewer pages.',
303
+ }
304
+ encodedBytes += size
305
+ parts.push({ type: 'image', mediaType: 'image/jpeg', data: image.toString('base64') })
306
+ } catch {
307
+ parts.push({ type: 'text', text: unusable })
308
+ }
309
+ }
310
+ return {
311
+ ok: true,
312
+ parts,
313
+ facts: { pages: rendered.names.length, ...(total !== null ? { totalPages: total } : {}) },
314
+ }
315
+ } finally {
316
+ await rm(directory, { recursive: true, force: true }).catch(() => {})
317
+ }
318
+ }
319
+
320
+ function formatSize(n: number): string {
321
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`
322
+ if (n >= 1024) return `${Math.round(n / 1024)} KiB`
323
+ return `${n} B`
324
+ }
325
+
326
+ const NOTEBOOK_INVALID =
327
+ 'Notebook file is not a valid Jupyter notebook (top-level "cells" must be an array of cell objects, each with a string or string-array "source").'
328
+
329
+ interface NotebookCell {
330
+ readonly id?: unknown
331
+ readonly cell_type?: unknown
332
+ readonly source?: unknown
333
+ readonly execution_count?: unknown
334
+ readonly outputs?: unknown
335
+ }
336
+
337
+ function joined(value: unknown): string | null {
338
+ if (typeof value === 'string') return value
339
+ if (Array.isArray(value) && value.every((line) => typeof line === 'string')) return value.join('')
340
+ return null
341
+ }
342
+
343
+ function isRecord(value: unknown): value is Record<string, unknown> {
344
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
345
+ }
346
+
347
+ function outputParts(output: unknown):
348
+ | {
349
+ text: string
350
+ image: MediaImage | undefined
351
+ imageSize?: number
352
+ }
353
+ | undefined {
354
+ if (!isRecord(output)) return undefined
355
+ switch (output.output_type) {
356
+ case 'stream':
357
+ return { text: joined(output.text) ?? '', image: undefined }
358
+ case 'execute_result':
359
+ case 'display_data': {
360
+ const data = isRecord(output.data) ? output.data : {}
361
+ const rawImage = data['image/png'] ?? data['image/jpeg']
362
+ if (typeof rawImage === 'string' && rawImage.length > CELL_OUTPUT_MAX_CHARS)
363
+ return {
364
+ text: joined(data['text/plain']) ?? '',
365
+ image: undefined,
366
+ imageSize: rawImage.length,
367
+ }
368
+ return { text: joined(data['text/plain']) ?? '', image: notebookImage(data) }
369
+ }
370
+ case 'error': {
371
+ const trace = Array.isArray(output.traceback) ? output.traceback.join('\n') : ''
372
+ return {
373
+ text: `${String(output.ename ?? '')}: ${String(output.evalue ?? '')}\n${trace}`,
374
+ image: undefined,
375
+ }
376
+ }
377
+ default:
378
+ return undefined
379
+ }
380
+ }
381
+
382
+ function notebookImage(data: Record<string, unknown>): MediaImage | undefined {
383
+ for (const mediaType of ['image/png', 'image/jpeg'] as const) {
384
+ const value = data[mediaType]
385
+ if (typeof value !== 'string') continue
386
+ const base64 = value.replace(/\s/g, '')
387
+ if (base64.length > NOTEBOOK_MAX_OUTPUT_BYTES || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64))
388
+ return undefined
389
+ try {
390
+ const decoded = Buffer.from(base64, 'base64')
391
+ if (decoded.toString('base64') !== base64) return undefined
392
+ describeRaster(decoded)
393
+ } catch {
394
+ return undefined
395
+ }
396
+ return { mediaType, data: base64 }
397
+ }
398
+ return undefined
399
+ }
400
+
401
+ /**
402
+ * A notebook as text the model can work from: every cell as
403
+ * `<cell id="...">source</cell id="...">` (a non-code cell names its type, a
404
+ * code cell in a language other than Python names the language), each followed
405
+ * by what it printed. A cell whose output is over 10,000 characters keeps only a
406
+ * note on how to read it.
407
+ */
408
+ export function readNotebook(bytes: Buffer, label: string): MediaRead {
409
+ if (bytes.length > NOTEBOOK_MAX_BYTES) {
410
+ return {
411
+ ok: false,
412
+ message: `Notebook file (${formatSize(bytes.length)}) exceeds the maximum size this tool can read (${formatSize(NOTEBOOK_MAX_BYTES)}). Use a shell command with jq to read specific portions, for example: jq '.cells[:20]' ${label}`,
413
+ }
414
+ }
415
+ let parsed: unknown
416
+ try {
417
+ parsed = JSON.parse(bytes.toString('utf-8'))
418
+ } catch (error) {
419
+ return {
420
+ ok: false,
421
+ message: `Notebook file is not valid JSON (it may be truncated, corrupted, or still being written): ${error instanceof Error ? error.message : String(error)}`,
422
+ }
423
+ }
424
+ if (!isRecord(parsed) || !Array.isArray(parsed.cells))
425
+ return { ok: false, message: NOTEBOOK_INVALID }
426
+ const cells = parsed.cells as unknown[]
427
+ if (!cells.every((cell) => isRecord(cell) && joined((cell as NotebookCell).source) !== null)) {
428
+ return { ok: false, message: NOTEBOOK_INVALID }
429
+ }
430
+ const metadata = isRecord(parsed.metadata) ? parsed.metadata : {}
431
+ const info = isRecord(metadata.language_info) ? metadata.language_info : {}
432
+ const language = typeof info.name === 'string' ? info.name : 'python'
433
+ const parts: MediaPart[] = []
434
+ let outputBytes = 0
435
+ let exceeded = false
436
+ const pushText = (text: string): void => {
437
+ outputBytes += Buffer.byteLength(text, 'utf8') + (parts.length > 0 ? 1 : 0)
438
+ if (outputBytes > NOTEBOOK_MAX_OUTPUT_BYTES) {
439
+ exceeded = true
440
+ return
441
+ }
442
+ parts.push({ type: 'text', text })
443
+ }
444
+ for (const [index, raw] of (cells as NotebookCell[]).entries()) {
445
+ const id = typeof raw.id === 'string' ? raw.id : `cell-${index}`
446
+ const type = typeof raw.cell_type === 'string' ? raw.cell_type : 'code'
447
+ const tags =
448
+ (type !== 'code' ? `<cell_type>${type}</cell_type>` : '') +
449
+ (type === 'code' && language !== 'python' ? `<language>${language}</language>` : '')
450
+ pushText(`<cell id="${id}">${tags}${joined(raw.source)}</cell id="${id}">`)
451
+ if (exceeded) break
452
+ if (type !== 'code' || !Array.isArray(raw.outputs) || raw.outputs.length === 0) continue
453
+ const outputs = raw.outputs.map(outputParts).filter((o) => o !== undefined)
454
+ const size = outputs.reduce(
455
+ (sum, o) => sum + o.text.length + (o.imageSize ?? o.image?.data.length ?? 0),
456
+ 0,
457
+ )
458
+ if (size > CELL_OUTPUT_MAX_CHARS) {
459
+ pushText(
460
+ `Outputs are too large to include. Use a shell command with: jq '.cells[${index}].outputs' ${label}`,
461
+ )
462
+ continue
463
+ }
464
+ for (const output of outputs) {
465
+ if (output.text) pushText(`\n${output.text}`)
466
+ if (output.image) {
467
+ outputBytes += output.image.data.length
468
+ if (outputBytes > NOTEBOOK_MAX_OUTPUT_BYTES) {
469
+ exceeded = true
470
+ break
471
+ }
472
+ parts.push({ type: 'image', ...output.image })
473
+ }
474
+ }
475
+ }
476
+ if (exceeded)
477
+ return {
478
+ ok: false,
479
+ message:
480
+ 'Notebook exceeds the 1 MiB encoded output budget. Extract selected cells with jq and read the extracted file.',
481
+ }
482
+ return { ok: true, parts, facts: { cells: cells.length } }
483
+ }
@@ -21,7 +21,7 @@ export const SearchToolsTool = defineTool({
21
21
  name: 'search_tools',
22
22
  description:
23
23
  'Search and load available tools by name or capability. ' +
24
- 'Deferred tools listed in the system prompt must be loaded via this tool before use.',
24
+ 'Use this only when a needed deferred tool schema is absent from the current runtime tools parameter. If its schema is already visible, call the tool directly without searching again.',
25
25
  inputSchema,
26
26
  category: 'analysis',
27
27
  permissions: [],
@@ -4,6 +4,7 @@ import { z } from 'zod'
4
4
  import { parseAllowedTools } from '../../authorization/skill-grant.js'
5
5
  import type { Sandbox } from '../../types/sandbox/index.js'
6
6
  import { isInvocableBy, skillInvocation } from '../../types/skills/index.js'
7
+ import type { ToolContext } from '../../types/tool/index.js'
7
8
  import { defineTool } from '../defineTool.js'
8
9
 
9
10
  export { parseAllowedTools }
@@ -88,6 +89,13 @@ const inputSchema = z.object({
88
89
  .describe(
89
90
  'The skill to load, exactly as listed in the available-skills manifest. Omit it to list model-invocable skills.',
90
91
  ),
92
+ args: z
93
+ .string()
94
+ .max(8000)
95
+ .optional()
96
+ .describe(
97
+ 'What to apply the skill to, in your own words. A skill that runs as a delegated agent sees only this and its own instructions, so say what it needs. Omit it on a continuation.',
98
+ ),
91
99
  cursor: z
92
100
  .string()
93
101
  .max(160)
@@ -306,6 +314,9 @@ function pageSkillBody(input: {
306
314
 
307
315
  export const SKILL_TOOL_NAME = 'skill'
308
316
 
317
+ /** The tool a skill that sets `context: fork` is handed to. */
318
+ const DELEGATION_TOOL_NAME = 'Agent'
319
+
309
320
  /**
310
321
  * Build the `skill` tool, with what the host knows about where its model's
311
322
  * tools run. {@link SkillTool} is this with no options.
@@ -471,6 +482,40 @@ export function createSkillTool(options: SkillToolOptions = {}) {
471
482
  }
472
483
  }
473
484
 
485
+ const argsLine =
486
+ input.args !== undefined && input.args.trim() !== ''
487
+ ? `Arguments: ${input.args.trim()}\n\n`
488
+ : ''
489
+
490
+ // A skill that asks to run as a delegated agent is handed to the turn's
491
+ // delegation tool, with the agent type, model and effort it names. It
492
+ // is only attempted on a first call: a continuation belongs to an
493
+ // inline load. Without that tool this turn the skill loads inline, and
494
+ // says so, instead of failing.
495
+ let forkUnavailable = ''
496
+ if (skill.metadata.context === 'fork' && input.cursor === undefined) {
497
+ if (context.dispatchTool && context.toolRegistry?.has(DELEGATION_TOOL_NAME)) {
498
+ return runForkedSkill({
499
+ name: input.name,
500
+ skill,
501
+ argsLine,
502
+ dispatch: context.dispatchTool,
503
+ header: resolveModelDirectory
504
+ ? directoryHeader(
505
+ nonEmpty(
506
+ await resolveModelDirectory(
507
+ { name: input.name, directory: skill.dirPath },
508
+ directoryContextOf(context.sandbox),
509
+ ),
510
+ ),
511
+ )
512
+ : '',
513
+ })
514
+ }
515
+ forkUnavailable =
516
+ '[This skill is written to run as a delegated agent, but this turn cannot delegate. Follow its instructions yourself.]\n\n'
517
+ }
518
+
474
519
  const allowed = parseAllowedTools(skill.metadata.allowedTools)
475
520
  // Asked on every call, first page or continuation: the digest binds
476
521
  // the answer, so a mount that changed between pages is a stale cursor
@@ -491,7 +536,7 @@ export function createSkillTool(options: SkillToolOptions = {}) {
491
536
  // pattern is matched against a command line the model writes, and it
492
537
  // writes the path it was told.
493
538
  skillDirectory: resolveModelDirectory ? modelDirectory : skill.dirPath,
494
- header: resolveModelDirectory ? directoryHeader(modelDirectory) : '',
539
+ header: forkUnavailable + (resolveModelDirectory ? directoryHeader(modelDirectory) : ''),
495
540
  invocation,
496
541
  }
497
542
  const digest = snapshotDigest(snapshot)
@@ -528,7 +573,8 @@ export function createSkillTool(options: SkillToolOptions = {}) {
528
573
  }
529
574
  const notice = grantNotice(allowed, grant, context.grantSkillTools !== undefined)
530
575
  const page = pageSkillBody({
531
- snapshot,
576
+ snapshot:
577
+ start === 0 && argsLine ? { ...snapshot, header: argsLine + snapshot.header } : snapshot,
532
578
  digest,
533
579
  start,
534
580
  notice,
@@ -619,3 +665,46 @@ function grantNotice(
619
665
  lines.push(unchanged)
620
666
  return `\n\n[${lines.join(' ')}]`
621
667
  }
668
+
669
+ /**
670
+ * Run a `context: fork` skill through the turn's delegation tool.
671
+ *
672
+ * The delegated agent starts with none of the conversation, so its prompt is
673
+ * the skill's instructions plus the arguments the model gave. The skill's
674
+ * `agent`, `model`, `effort` and `background` pass straight through. The host's
675
+ * delegation tool decides whether effort without a model is supported. Never
676
+ * discard the request or invent a model to make it pass host validation.
677
+ */
678
+ async function runForkedSkill(input: {
679
+ readonly name: string
680
+ readonly skill: {
681
+ readonly metadata: {
682
+ readonly agent?: string
683
+ readonly model?: string
684
+ readonly effort?: string
685
+ readonly background?: boolean
686
+ }
687
+ readonly body?: string
688
+ }
689
+ readonly argsLine: string
690
+ readonly header: string
691
+ readonly dispatch: NonNullable<ToolContext['dispatchTool']>
692
+ }) {
693
+ const { metadata, body } = input.skill
694
+ const result = await input.dispatch(DELEGATION_TOOL_NAME, {
695
+ description: `Skill ${input.name}`,
696
+ prompt: `${input.header}${input.argsLine}${body ?? '(this skill has no body)'}`,
697
+ ...(metadata.agent === undefined ? {} : { subagent_type: metadata.agent }),
698
+ ...(metadata.model === undefined ? {} : { model: metadata.model }),
699
+ ...(metadata.effort === undefined ? {} : { effort: metadata.effort }),
700
+ ...(metadata.background === undefined ? {} : { run_in_background: metadata.background }),
701
+ })
702
+ return {
703
+ ...result,
704
+ data: {
705
+ ...(typeof result.data === 'object' && result.data !== null ? result.data : {}),
706
+ skill: input.name,
707
+ forked: true,
708
+ },
709
+ }
710
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A leading `sleep` of this many seconds or more is refused where a better
3
+ * way to wait exists (a background job to wait on, or a monitor).
4
+ */
5
+ export const LONG_SLEEP_SECONDS = 25
6
+
7
+ /**
8
+ * Describes the long wait a command opens with, or `null` when it does not
9
+ * open with one. Only a first sub-command that is exactly `sleep <seconds>`
10
+ * counts; a sleep in the middle of a chain, or a suffixed one (`sleep 30s`),
11
+ * is left alone.
12
+ */
13
+ export function leadingLongSleep(command: string): string | null {
14
+ const parts = command
15
+ .split(/&&|\|\||;|\||\n/)
16
+ .map((part) => part.trim())
17
+ .filter((part) => part !== '')
18
+ const head = parts[0]
19
+ if (head === undefined) return null
20
+ const match = /^sleep\s+(\d+(?:\.\d*)?)$/.exec(head)
21
+ if (!match) return null
22
+ const seconds = Number.parseFloat(match[1] ?? '')
23
+ if (!(seconds >= LONG_SLEEP_SECONDS)) return null
24
+ const rest = parts.slice(1).join(' ').trim()
25
+ return rest ? `sleep ${seconds} followed by: ${rest}` : `standalone sleep ${seconds}`
26
+ }
27
+
28
+ /** What the model is told when a long leading sleep is refused. */
29
+ export function longSleepRefusal(what: string): string {
30
+ return `Blocked: ${what}. To wait for a condition, use the monitor tool with an until-loop (for example \`until <check>; do sleep 2; done\`). To wait for a command you started, use run_in_background: true and then wait_for_job. Do not chain shorter sleeps to work around this block.`
31
+ }