minimal-agent 0.11.0 → 0.13.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 (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -1,20 +1,52 @@
1
1
  import { createReadStream } from 'node:fs';
2
- import { open, readFile, stat } from 'node:fs/promises';
2
+ import { open, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { extname } from 'node:path';
4
4
  import { createInterface } from 'node:readline';
5
+ import { execFile } from 'node:child_process';
6
+ import { mkdir } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { promisify } from 'node:util';
9
+ import { randomUUID } from 'node:crypto';
5
10
  import { z } from 'zod';
6
11
  import { toToolParameters } from '../../utils/zodToJson.js';
7
- import { DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_FILE_SIZE_BYTES, MAX_LINES_TO_READ } from '../types.js';
12
+ import { DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_FILE_SIZE_BYTES, MAX_LINES_TO_READ, } from '../types.js';
8
13
  import { fileActivityMetadata } from '../shared/fileActivity.js';
9
- import { hashFileBytes, recordRead } from '../shared/fileState.js';
14
+ import { hashFileBytes, recordRead, } from '../shared/fileState.js';
10
15
  import { hasBinaryExtension, isBlockedDevicePath, readTextFile, resolveToolPath, } from '../shared/fileUtils.js';
11
16
  import { filePathField } from '../shared/schemas.js';
17
+ import { pruneRuntimeOutputs, sessionRuntimeDir, } from '../../session/runtimeStore.js';
12
18
  const inputSchema = z.object({
13
19
  file_path: filePathField('读取'),
14
- offset: z.number().int().positive().optional().describe('文本文件为 1-indexed 起始行;Jupyter Notebook 为 1-indexed 起始 cell'),
15
- limit: z.number().int().positive().optional().describe(`最多读取的文本行或 Notebook cells;默认 ${MAX_LINES_TO_READ}`),
20
+ offset: z
21
+ .number()
22
+ .int()
23
+ .positive()
24
+ .optional()
25
+ .describe('文本文件为 1-indexed 起始行;Jupyter Notebook 为 1-indexed 起始 cell'),
26
+ limit: z
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .optional()
31
+ .describe(`最多读取的文本行或 Notebook cells;默认 ${MAX_LINES_TO_READ}`),
32
+ pages: z
33
+ .string()
34
+ .regex(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/)
35
+ .optional()
36
+ .describe('PDF 页码,例如 "1-5,8";每次最多 20 页'),
37
+ pdf_mode: z
38
+ .enum(['auto', 'text', 'visual', 'both'])
39
+ .default('auto')
40
+ .optional(),
16
41
  });
17
- const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
42
+ const IMAGE_EXTENSIONS = new Set([
43
+ '.png',
44
+ '.jpg',
45
+ '.jpeg',
46
+ '.gif',
47
+ '.webp',
48
+ '.bmp',
49
+ ]);
18
50
  const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
19
51
  const MAX_NOTEBOOK_BYTES = 32 * 1024 * 1024;
20
52
  const SMALL_TEXT_BYTES = 16 * 1024 * 1024;
@@ -23,13 +55,15 @@ const description = `Reads local text files, images, and Jupyter notebooks.
23
55
  Usage:
24
56
  - Relative paths resolve against the current working directory.
25
57
  - Text results use 1-based line numbers. Use offset/limit for targeted ranges.
26
- - PNG, JPEG, WebP, GIF, BMP, and TIFF files are attached for visual inspection.
58
+ - PNG, JPEG, WebP, GIF, and BMP files are attached for visual inspection.
27
59
  - For .ipynb files, offset/limit select cells; results include cell id, type, source, and bounded outputs.
28
- - PDF and office files are not decoded by this tool. Use the matching project Skill for those formats.
60
+ - PDF supports page-selected text extraction and optional visual page rendering; office files use the matching project Skill.
29
61
  - This tool reads files, not directories; use Glob for directory discovery.`;
30
62
  function notebookSource(value) {
31
63
  if (Array.isArray(value))
32
- return value.filter((item) => typeof item === 'string').join('');
64
+ return value
65
+ .filter((item) => typeof item === 'string')
66
+ .join('');
33
67
  return typeof value === 'string' ? value : '';
34
68
  }
35
69
  function renderNotebook(raw, offset, limit) {
@@ -39,17 +73,24 @@ function renderNotebook(raw, offset, limit) {
39
73
  const start = offset - 1;
40
74
  const cells = parsed.cells.slice(start, start + limit);
41
75
  const parts = cells.map((value, index) => {
42
- const cell = value && typeof value === 'object' ? value : {};
76
+ const cell = value && typeof value === 'object'
77
+ ? value
78
+ : {};
43
79
  const outputs = Array.isArray(cell.outputs) ? cell.outputs : [];
44
- const renderedOutputs = outputs.slice(0, 8).map((output) => {
80
+ const renderedOutputs = outputs
81
+ .slice(0, 8)
82
+ .map((output) => {
45
83
  if (!output || typeof output !== 'object')
46
84
  return String(output ?? '');
47
85
  const item = output;
48
- const text = item.text ?? (item.data && typeof item.data === 'object'
49
- ? item.data['text/plain']
50
- : undefined);
86
+ const text = item.text ??
87
+ (item.data && typeof item.data === 'object'
88
+ ? item.data['text/plain']
89
+ : undefined);
51
90
  return notebookSource(text).slice(0, 2_000);
52
- }).filter(Boolean).join('\n');
91
+ })
92
+ .filter(Boolean)
93
+ .join('\n');
53
94
  return [
54
95
  `## Cell ${start + index + 1} [selector=cell-${start + index + 1}${typeof cell.id === 'string' ? `; id=${cell.id}` : ''}; type=${String(cell.cell_type ?? 'unknown')}]`,
55
96
  notebookSource(cell.source),
@@ -59,7 +100,11 @@ function renderNotebook(raw, offset, limit) {
59
100
  const end = start + cells.length;
60
101
  if (end < parsed.cells.length)
61
102
  parts.push(`... (${parsed.cells.length - end} cells remain; continue with offset=${end + 1})`);
62
- return { content: parts.join('\n\n'), total: parsed.cells.length, partial: start > 0 || end < parsed.cells.length };
103
+ return {
104
+ content: parts.join('\n\n'),
105
+ total: parsed.cells.length,
106
+ partial: start > 0 || end < parsed.cells.length,
107
+ };
63
108
  }
64
109
  async function readStreamedText(filePath, offset, limit) {
65
110
  const handle = await open(filePath, 'r');
@@ -69,7 +114,14 @@ async function readStreamedText(filePath, offset, limit) {
69
114
  const utf16 = bytesRead >= 2 && prefix[0] === 0xff && prefix[1] === 0xfe;
70
115
  const stream = createReadStream(filePath, {
71
116
  encoding: utf16 ? 'utf16le' : 'utf8',
72
- start: utf16 ? 2 : bytesRead >= 3 && prefix[0] === 0xef && prefix[1] === 0xbb && prefix[2] === 0xbf ? 3 : 0,
117
+ start: utf16
118
+ ? 2
119
+ : bytesRead >= 3 &&
120
+ prefix[0] === 0xef &&
121
+ prefix[1] === 0xbb &&
122
+ prefix[2] === 0xbf
123
+ ? 3
124
+ : 0,
73
125
  });
74
126
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
75
127
  const lines = [];
@@ -88,7 +140,7 @@ async function readStreamedText(filePath, offset, limit) {
88
140
  }
89
141
  return { numbered: lines.join('\n'), totalKnown: line, hasMore };
90
142
  }
91
- async function call(input, state, cwd) {
143
+ async function call(input, state, cwd, runtimeIdentity) {
92
144
  const offset = input.offset ?? 1;
93
145
  const limit = input.limit ?? MAX_LINES_TO_READ;
94
146
  const resolved = resolveToolPath(input.file_path, cwd);
@@ -99,10 +151,80 @@ async function call(input, state, cwd) {
99
151
  return { ok: false, error: `不允许读取设备文件:${filePath}` };
100
152
  const info = await stat(filePath).catch((error) => ({ error }));
101
153
  if ('error' in info)
102
- return { ok: false, error: `文件不存在或无法读取:${filePath}(${info.error.message})` };
154
+ return {
155
+ ok: false,
156
+ error: `文件不存在或无法读取:${filePath}(${info.error.message})`,
157
+ };
103
158
  if (!info.isFile())
104
159
  return { ok: false, error: `${filePath} 不是文件;目录请用 Glob。` };
105
160
  const ext = extname(filePath).toLowerCase();
161
+ if (ext === '.pdf') {
162
+ try {
163
+ const bytes = await readFile(filePath);
164
+ if (bytes.subarray(0, 5).toString() !== '%PDF-') {
165
+ return {
166
+ ok: false,
167
+ error: '文件扩展名是 PDF,但缺少有效 %PDF- 文件头',
168
+ };
169
+ }
170
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
171
+ // ⚠️ 释放要调**加载任务**的 destroy,不是文档代理的:pdfjs v6 的
172
+ // PDFDocumentProxy 上没有 destroy(只有 cleanup),写成 document.destroy()
173
+ // 会在 finally 里抛 TypeError,被外层 catch 成「PDF 解析失败」——
174
+ // 于是每一次 PDF 读取都失败,且错误文案完全指不向真因。
175
+ const loadingTask = pdfjs.getDocument({
176
+ data: new Uint8Array(bytes),
177
+ useSystemFonts: true,
178
+ });
179
+ const document = await loadingTask.promise;
180
+ try {
181
+ const requested = parsePdfPages(input.pages, document.numPages);
182
+ if (requested.length > 20)
183
+ return { ok: false, error: '一次最多读取 20 个 PDF 页面' };
184
+ const chunks = [];
185
+ for (const pageNumber of requested) {
186
+ const page = await document.getPage(pageNumber);
187
+ const text = await page.getTextContent();
188
+ chunks.push(`## Page ${pageNumber}\n${text.items
189
+ .map((item) => 'str' in item && typeof item.str === 'string' ? item.str : '')
190
+ .filter(Boolean)
191
+ .join(' ')}`);
192
+ }
193
+ const mode = input.pdf_mode ?? 'auto';
194
+ const sparse = chunks.join('').replace(/\s/g, '').length < requested.length * 80;
195
+ const wantVisual = mode === 'visual' || mode === 'both' || (mode === 'auto' && sparse);
196
+ const images = wantVisual
197
+ ? await renderPdfPages(document, filePath, requested.slice(0, 10), runtimeIdentity)
198
+ : [];
199
+ recordRead(filePath, { contentHash: hashFileBytes(bytes) }, state);
200
+ const rawText = mode === 'visual' ? '[visual-only mode]' : chunks.join('\n\n');
201
+ const text = rawText.length > DEFAULT_MAX_RESULT_SIZE_CHARS
202
+ ? `${rawText.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (PDF 文本输出已截断;请缩小 pages 范围继续读取)`
203
+ : rawText;
204
+ return {
205
+ ok: true,
206
+ content: `PDF ${document.numPages} pages; selected ${requested.join(', ')}.\n\n${text}${wantVisual && !images.length ? '\n\n视觉渲染不可用(Canvas 与 pdftoppm 均不可用);文本提取结果仍可用。' : ''}`,
207
+ metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
208
+ ...(images.length
209
+ ? {
210
+ visionImages: images.map((source) => ({ source })),
211
+ visionNote: '这是所选 PDF 页面的视觉渲染,请检查版式、图表和扫描内容。',
212
+ }
213
+ : {}),
214
+ };
215
+ }
216
+ finally {
217
+ await loadingTask.destroy().catch(() => undefined);
218
+ }
219
+ }
220
+ catch (error) {
221
+ const message = error.message;
222
+ return {
223
+ ok: false,
224
+ error: `PDF 解析失败${/password/i.test(message) ? '(文件可能已加密)' : ''}:${message}`,
225
+ };
226
+ }
227
+ }
106
228
  if (IMAGE_EXTENSIONS.has(ext)) {
107
229
  if (info.size > MAX_IMAGE_BYTES)
108
230
  return { ok: false, error: `图片超过 ${MAX_IMAGE_BYTES} 字节上限` };
@@ -118,7 +240,10 @@ async function call(input, state, cwd) {
118
240
  }
119
241
  if (ext === '.ipynb') {
120
242
  if (info.size > MAX_NOTEBOOK_BYTES)
121
- return { ok: false, error: `Notebook 超过 ${MAX_NOTEBOOK_BYTES} 字节上限` };
243
+ return {
244
+ ok: false,
245
+ error: `Notebook 超过 ${MAX_NOTEBOOK_BYTES} 字节上限`,
246
+ };
122
247
  try {
123
248
  const snapshot = await readTextFile(filePath);
124
249
  const rendered = renderNotebook(snapshot.content, offset, Math.min(limit, 200));
@@ -133,19 +258,34 @@ async function call(input, state, cwd) {
133
258
  limit,
134
259
  partial: rendered.partial,
135
260
  truncated,
136
- ...(!rendered.partial && !truncated ? { contentHash: hashFileBytes(snapshot.bytes) } : {}),
261
+ ...(!rendered.partial && !truncated
262
+ ? { contentHash: hashFileBytes(snapshot.bytes) }
263
+ : {}),
137
264
  }, state);
138
- return { ok: true, content, metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd) };
265
+ return {
266
+ ok: true,
267
+ content,
268
+ metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
269
+ };
139
270
  }
140
271
  catch (error) {
141
- return { ok: false, error: `Notebook 解析失败:${error.message}` };
272
+ return {
273
+ ok: false,
274
+ error: `Notebook 解析失败:${error.message}`,
275
+ };
142
276
  }
143
277
  }
144
278
  if (hasBinaryExtension(filePath)) {
145
- return { ok: false, error: `不支持直接读取 ${ext || '该二进制格式'};PDF/Office 文件请使用对应 Skill。` };
279
+ return {
280
+ ok: false,
281
+ error: `不支持直接读取 ${ext || '该二进制格式'};PDF/Office 文件请使用对应 Skill。`,
282
+ };
146
283
  }
147
284
  if (info.size > MAX_FILE_SIZE_BYTES) {
148
- return { ok: false, error: `文件超过 ${MAX_FILE_SIZE_BYTES} 字节硬上限。请用 Grep 定位,或用 Bash 做流式处理。` };
285
+ return {
286
+ ok: false,
287
+ error: `文件超过 ${MAX_FILE_SIZE_BYTES} 字节硬上限。请用 Grep 定位,或用 Bash 做流式处理。`,
288
+ };
149
289
  }
150
290
  try {
151
291
  let content;
@@ -158,7 +298,9 @@ async function call(input, state, cwd) {
158
298
  const all = snapshot.content.length === 0 ? [] : snapshot.content.split('\n');
159
299
  const start = offset - 1;
160
300
  const selected = all.slice(start, start + limit);
161
- content = selected.map((line, index) => `${start + index + 1}\t${line}`).join('\n');
301
+ content = selected
302
+ .map((line, index) => `${start + index + 1}\t${line}`)
303
+ .join('\n');
162
304
  totalLines = all.length;
163
305
  hasMore = start + selected.length < all.length;
164
306
  }
@@ -183,19 +325,97 @@ async function call(input, state, cwd) {
183
325
  limit,
184
326
  partial,
185
327
  truncated,
186
- ...(!partial && !truncated && completeBytes ? { contentHash: hashFileBytes(completeBytes) } : {}),
328
+ ...(!partial && !truncated && completeBytes
329
+ ? { contentHash: hashFileBytes(completeBytes) }
330
+ : {}),
187
331
  }, state);
188
332
  if (info.size > 100 * 1024 && offset === 1) {
189
333
  content = `⚠️ 大文件:结果受行数和输出字符上限约束,请按需分段读取。\n\n${content}`;
190
334
  }
191
335
  if (reads >= 2)
192
336
  content = `⚠️ 重复读取空转:该区间未变化,前一次 Read 结果仍然有效。\n\n${content}`;
193
- return { ok: true, content, metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd) };
337
+ return {
338
+ ok: true,
339
+ content,
340
+ metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
341
+ };
194
342
  }
195
343
  catch (error) {
196
344
  return { ok: false, error: `读取失败:${error.message}` };
197
345
  }
198
346
  }
347
+ async function renderPdfPages(document, filePath, pages, identity) {
348
+ const outputDir = join(sessionRuntimeDir(identity ?? {}), 'outputs');
349
+ const outputPrefix = `pdf_${randomUUID()}`;
350
+ await mkdir(outputDir, { recursive: true, mode: 0o700 });
351
+ const images = [];
352
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
353
+ let createCanvas;
354
+ try {
355
+ ({ createCanvas } = await dynamicImport('@napi-rs/canvas'));
356
+ }
357
+ catch {
358
+ /* optional adapter */
359
+ }
360
+ if (createCanvas) {
361
+ try {
362
+ for (const pageNumber of pages) {
363
+ const page = await document.getPage(pageNumber);
364
+ const viewport = page.getViewport({ scale: 2 });
365
+ const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
366
+ await page.render({
367
+ canvas: canvas.getContext('2d'),
368
+ viewport,
369
+ }).promise;
370
+ const target = join(outputDir, `${outputPrefix}-page-${pageNumber}.png`);
371
+ await writeFile(target, canvas.toBuffer('image/png'), { mode: 0o600 });
372
+ images.push(target);
373
+ }
374
+ void pruneRuntimeOutputs();
375
+ return images;
376
+ }
377
+ catch {
378
+ images.length = 0;
379
+ }
380
+ }
381
+ for (const pageNumber of pages) {
382
+ const prefix = join(outputDir, `${outputPrefix}-page-${pageNumber}`);
383
+ try {
384
+ await promisify(execFile)('pdftoppm', [
385
+ '-f',
386
+ String(pageNumber),
387
+ '-l',
388
+ String(pageNumber),
389
+ '-singlefile',
390
+ '-png',
391
+ '-r',
392
+ '144',
393
+ filePath,
394
+ prefix,
395
+ ], { maxBuffer: 1024 * 1024 });
396
+ images.push(`${prefix}.png`);
397
+ }
398
+ catch {
399
+ break;
400
+ }
401
+ }
402
+ void pruneRuntimeOutputs();
403
+ return images;
404
+ }
405
+ function parsePdfPages(spec, total) {
406
+ if (!spec)
407
+ return Array.from({ length: Math.min(total, 20) }, (_, index) => index + 1);
408
+ const pages = new Set();
409
+ for (const part of spec.split(',')) {
410
+ const [first, last] = part.split('-').map(Number);
411
+ for (let page = first; page <= (last ?? first); page += 1) {
412
+ if (page < 1 || page > total)
413
+ throw new Error(`PDF 页码超出范围:${page}(共 ${total} 页)`);
414
+ pages.add(page);
415
+ }
416
+ }
417
+ return [...pages].sort((left, right) => left - right);
418
+ }
199
419
  export const readTool = {
200
420
  name: 'Read',
201
421
  description,
@@ -206,5 +426,5 @@ export const readTool = {
206
426
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
207
427
  interruptBehavior: () => 'cancel',
208
428
  call: (input) => call(input),
209
- callWithContext: (input, _signal, context) => call(input, context.sessionState?.fileReads, context.cwd),
429
+ callWithContext: (input, _signal, context) => call(input, context.sessionState?.fileReads, context.cwd, context),
210
430
  };