@sema-agent/core 1.452.0 → 2.0.1

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 (167) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/repair-loop.d.ts +1 -1
  5. package/dist/agents/repair-loop.js +1 -1
  6. package/dist/agents/retain-ledger.d.ts +60 -0
  7. package/dist/agents/retain-ledger.js +225 -0
  8. package/dist/agents/roster-store.d.ts +1 -0
  9. package/dist/agents/roster-store.js +9 -0
  10. package/dist/agents/send-message-tool.d.ts +41 -0
  11. package/dist/agents/send-message-tool.js +469 -0
  12. package/dist/agents/subagent.d.ts +33 -108
  13. package/dist/agents/subagent.js +22 -834
  14. package/dist/agents/teacher.d.ts +0 -1
  15. package/dist/agents/teacher.js +0 -9
  16. package/dist/bin/sema-tb.js +3 -3
  17. package/dist/brain/anthropic.js +15 -40
  18. package/dist/brain/openai.js +15 -40
  19. package/dist/brain/reasoning.d.ts +1 -1
  20. package/dist/brain/stream-engine.js +2 -2
  21. package/dist/brain/stream-shared.d.ts +17 -0
  22. package/dist/brain/stream-shared.js +50 -0
  23. package/dist/brain/timeout.d.ts +1 -1
  24. package/dist/brain/timeout.js +1 -1
  25. package/dist/config/catalog.js +1 -3
  26. package/dist/config/defaults.d.ts +6 -0
  27. package/dist/config/defaults.js +6 -0
  28. package/dist/core/ask-question.d.ts +1 -1
  29. package/dist/core/background-shell.d.ts +1 -1
  30. package/dist/core/checkpoint-store.d.ts +0 -1
  31. package/dist/core/exec-gate.d.ts +1 -1
  32. package/dist/core/exec-output-tail.d.ts +1 -0
  33. package/dist/core/exec-output-tail.js +6 -0
  34. package/dist/core/file-snapshot-store.d.ts +1 -1
  35. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  36. package/dist/core/git-worktree-env.d.ts +1 -1
  37. package/dist/core/hooks.d.ts +1 -1
  38. package/dist/core/image-downsample.d.ts +44 -0
  39. package/dist/core/image-downsample.js +97 -0
  40. package/dist/core/lsp.d.ts +1 -1
  41. package/dist/core/mcp.d.ts +3 -45
  42. package/dist/core/mcp.js +2 -97
  43. package/dist/core/media-byte-cap.d.ts +1 -1
  44. package/dist/core/memory-recall.d.ts +0 -12
  45. package/dist/core/memory-recall.js +0 -222
  46. package/dist/core/memory.d.ts +0 -6
  47. package/dist/core/memory.js +0 -88
  48. package/dist/core/message-utils.d.ts +1 -1
  49. package/dist/core/oracle-isolation.d.ts +1 -1
  50. package/dist/core/remote-env.d.ts +1 -1
  51. package/dist/core/roles.d.ts +1 -1
  52. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  53. package/dist/core/runner/assemble-result.d.ts +0 -2
  54. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  55. package/dist/core/runner/prepare-memory.d.ts +16 -0
  56. package/dist/core/runner/prepare-memory.js +191 -0
  57. package/dist/core/runner/prepare-task.d.ts +4 -28
  58. package/dist/core/runner/prepare-task.js +19 -207
  59. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  60. package/dist/core/runner/runtask.d.ts +2 -3
  61. package/dist/core/runner/runtask.js +337 -529
  62. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  63. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  64. package/dist/core/runner/synthetic-tools.js +4 -4
  65. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  66. package/dist/core/runner/tool-disclosure.js +2 -2
  67. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  68. package/dist/core/runner/tool-output-projection.js +81 -0
  69. package/dist/core/runtime.d.ts +1 -1
  70. package/dist/core/scheduler.d.ts +1 -1
  71. package/dist/core/sensitive-path-policy.d.ts +1 -1
  72. package/dist/core/session-reconcile.d.ts +1 -1
  73. package/dist/core/session-store.d.ts +2 -1
  74. package/dist/core/session-store.js +2 -1
  75. package/dist/core/session.d.ts +0 -1
  76. package/dist/core/task-registry-agent.d.ts +115 -0
  77. package/dist/core/task-registry-agent.js +1163 -0
  78. package/dist/core/task-registry-monitor.d.ts +13 -0
  79. package/dist/core/task-registry-monitor.js +347 -0
  80. package/dist/core/task-registry-shared.d.ts +294 -0
  81. package/dist/core/task-registry-shared.js +271 -0
  82. package/dist/core/task-registry-workflow.d.ts +5 -0
  83. package/dist/core/task-registry-workflow.js +143 -0
  84. package/dist/core/task-registry.d.ts +51 -216
  85. package/dist/core/task-registry.js +127 -1940
  86. package/dist/core/task-tool-shape.d.ts +32 -0
  87. package/dist/core/task-tool-shape.js +83 -0
  88. package/dist/core/tool-policy.d.ts +1 -2
  89. package/dist/core/tool-result-budget.d.ts +1 -1
  90. package/dist/core/tool-result-store.d.ts +2 -2
  91. package/dist/core/tool-result-store.js +1 -1
  92. package/dist/core/tools.d.ts +2 -3
  93. package/dist/core/tools.js +1 -8
  94. package/dist/core/trace.d.ts +2 -2
  95. package/dist/core/types.d.ts +0 -29
  96. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  97. package/dist/core/workflow-run-store-contract.js +19 -19
  98. package/dist/engine/execution-env/node-execution-env.d.ts +13 -0
  99. package/dist/engine/execution-env/node-execution-env.js +55 -25
  100. package/dist/engine/loop/agent-loop.d.ts +1 -1
  101. package/dist/engine/loop/agent-loop.js +10 -9
  102. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  103. package/dist/engine/lsp/frame-decoder.js +1 -1
  104. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  105. package/dist/engine/lsp/stdio-lsp-transport.js +2 -2
  106. package/dist/index.d.ts +16 -14
  107. package/dist/index.js +13 -11
  108. package/dist/internal/harness-types.d.ts +7 -0
  109. package/dist/internal/harness-types.js +1 -0
  110. package/dist/internal/harness.d.ts +1 -7
  111. package/dist/internal/harness.js +1 -0
  112. package/dist/orchestration/goal.js +2 -2
  113. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  114. package/dist/orchestration/workflow-types.d.ts +193 -0
  115. package/dist/orchestration/workflow-types.js +54 -0
  116. package/dist/orchestration/workflow.d.ts +3 -193
  117. package/dist/orchestration/workflow.js +7 -59
  118. package/dist/prompt-assembly/assemble.d.ts +3 -3
  119. package/dist/prompt-assembly/assemble.js +0 -16
  120. package/dist/prompt-assembly/explain.d.ts +1 -1
  121. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  122. package/dist/prompts/default.d.ts +1 -6
  123. package/dist/prompts/default.js +3 -11
  124. package/dist/scenarios/env.d.ts +1 -1
  125. package/dist/scenarios/full-body.d.ts +0 -3
  126. package/dist/scenarios/full-body.js +0 -2
  127. package/dist/stores/file/background-agent-store.d.ts +2 -15
  128. package/dist/stores/file/background-agent-store.js +30 -100
  129. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  130. package/dist/stores/file/checkpoint-store.js +47 -119
  131. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  132. package/dist/stores/file/mailbox-store.js +33 -2
  133. package/dist/stores/file/shared-ledger.d.ts +35 -0
  134. package/dist/stores/file/shared-ledger.js +94 -0
  135. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  136. package/dist/stores/file/workflow-run-store.js +24 -93
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  138. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  139. package/dist/tools/fs/fs-bash.d.ts +31 -0
  140. package/dist/tools/fs/fs-bash.js +672 -0
  141. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  142. package/dist/tools/fs/fs-pdf.js +236 -0
  143. package/dist/tools/fs/fs-read.d.ts +8 -0
  144. package/dist/tools/fs/fs-read.js +286 -0
  145. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  146. package/dist/tools/fs/fs-search-tools.js +175 -0
  147. package/dist/tools/fs/fs-shared.d.ts +56 -0
  148. package/dist/tools/fs/fs-shared.js +148 -0
  149. package/dist/tools/fs/fs-write.d.ts +8 -0
  150. package/dist/tools/fs/fs-write.js +421 -0
  151. package/dist/tools/fs/index.d.ts +12 -65
  152. package/dist/tools/fs/index.js +26 -2090
  153. package/dist/tools/fs/pdf.d.ts +1 -1
  154. package/dist/tools/fs/repo-map.d.ts +3 -3
  155. package/dist/tools/fs/repo-map.js +1 -1
  156. package/dist/tools/fs/safety.d.ts +4 -1
  157. package/dist/tools/fs/safety.js +7 -1
  158. package/dist/tools/fs/search.d.ts +2 -1
  159. package/dist/tools/fs/search.js +21 -3
  160. package/dist/tools/monitor.d.ts +1 -1
  161. package/dist/tools/scheduler-tools.d.ts +1 -1
  162. package/dist/tools/web.d.ts +2 -2
  163. package/dist/tools/web.js +46 -8
  164. package/dist/tools/worktree.d.ts +1 -1
  165. package/package.json +1 -1
  166. package/dist/core/dynamic-recall.d.ts +0 -32
  167. package/dist/core/dynamic-recall.js +0 -76
@@ -0,0 +1,22 @@
1
+ import { errorResult } from "../../core/tools.js";
2
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
3
+ import { type PdfModelCapabilities } from "./pdf.js";
4
+ import { type ReadImageDownsamplerOption } from "./fs-shared.js";
5
+ export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
6
+ type ReadPdfReturn = string | {
7
+ content: Array<{
8
+ type: "text";
9
+ text: string;
10
+ } | {
11
+ type: "image";
12
+ data: string;
13
+ mimeType: string;
14
+ } | {
15
+ type: "document";
16
+ data: string;
17
+ mimeType: string;
18
+ }>;
19
+ details: unknown;
20
+ };
21
+ export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities): Promise<ReadPdfReturn>;
22
+ export declare function pdfResultToToolReturn(r: ReadPdfReturn): ReturnType<typeof errorResult> | Exclude<ReadPdfReturn, string>;
@@ -0,0 +1,236 @@
1
+ import { errorResult } from "../../core/tools.js";
2
+ import { delimitUntrusted } from "../../core/untrusted-text.js";
3
+ import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
4
+ import { PDF_FALLBACK_RENDER_PAGES, PDF_INLINE_PAGE_THRESHOLD, PDF_MAX_EXTRACT_SIZE, PDF_MAX_PAGES_PER_READ, PDF_TARGET_RAW_SIZE, extractPdfPagesAsImages, extractPdfTextLayer, getPdfPageCount, parsePdfPageRange, pdfMagicMatches, } from "./pdf.js";
5
+ import { resolveAutoDownsampler, enoentMessage, MAX_READ_BYTES } from "./fs-shared.js";
6
+ export { pdfModelCapabilitiesOf } from "./pdf.js";
7
+ async function pdfPagesToImageBlocks(pages, path, downsamplerOpt) {
8
+ const downsampler = downsamplerOpt === false ? undefined : (downsamplerOpt ?? (await resolveAutoDownsampler()));
9
+ const blocks = [];
10
+ for (const p of pages) {
11
+ let b64;
12
+ let mime = "image/jpeg";
13
+ if (downsampler) {
14
+ const out = await downsampler(Buffer.from(p.jpeg), "image/jpeg").catch(() => undefined);
15
+ if (out) {
16
+ b64 = out.base64;
17
+ mime = out.mimeType;
18
+ }
19
+ }
20
+ b64 ??= Buffer.from(p.jpeg).toString("base64");
21
+ if (b64.length > MCP_IMAGE_MAX_BASE64) {
22
+ return `Error (Read): rendered page ${p.pageNumber} of "${path}" is too large to send (base64 ${b64.length} chars exceeds the ${MCP_IMAGE_MAX_BASE64}-char / 5MB API image limit); narrow the range or extract text via Bash (e.g. \`pdftotext\`).`;
23
+ }
24
+ blocks.push({ type: "image", data: b64, mimeType: mime });
25
+ }
26
+ return blocks;
27
+ }
28
+ function boundPdfExtractedText(text) {
29
+ if (Buffer.byteLength(text, "utf8") <= MAX_READ_BYTES)
30
+ return { body: text, truncated: false };
31
+ return { body: text.slice(0, MAX_READ_BYTES), truncated: true };
32
+ }
33
+ export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, preRead, caps) {
34
+ const cap = caps ?? { document: true, vision: true };
35
+ const meta = await env.fileInfo(key, signal);
36
+ if (!meta.ok) {
37
+ const ex = await env.exists(key, signal);
38
+ if (ex.ok && !ex.value)
39
+ return `Error (Read): ${await enoentMessage(env, key, cwd, signal)}`;
40
+ return `Error (Read): cannot stat PDF "${path}" to verify its size before reading: ${meta.error.message}`;
41
+ }
42
+ if (meta.value.kind === "directory") {
43
+ return `Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`;
44
+ }
45
+ const size = preRead ? preRead.byteLength : meta.value.size;
46
+ if (size > PDF_MAX_EXTRACT_SIZE) {
47
+ return `Error (Read): PDF "${path}" exceeds the maximum size for PDF reading (${size} bytes > ${PDF_MAX_EXTRACT_SIZE}-byte / 100 MB cap).`;
48
+ }
49
+ let bytes = preRead;
50
+ if (bytes === undefined && size <= PDF_TARGET_RAW_SIZE) {
51
+ const bin = await env.readBinaryFile(key, signal);
52
+ if (!bin.ok)
53
+ return `Error (Read): cannot read PDF "${path}": ${bin.error.message}`;
54
+ bytes = bin.value;
55
+ }
56
+ if (bytes !== undefined && !pdfMagicMatches(bytes)) {
57
+ return `Error (Read): "${path}" has a .pdf extension but its content is not a valid PDF (no %PDF- header; empty, truncated, or mis-named).`;
58
+ }
59
+ if (pages !== undefined) {
60
+ const range = parsePdfPageRange(pages);
61
+ if (range === undefined) {
62
+ return `Error (Read): Invalid pages parameter: "${pages}". Use formats like "1-5", "3", or "10-20". Pages are 1-indexed.`;
63
+ }
64
+ if (range.last !== Infinity && range.last - range.first + 1 > PDF_MAX_PAGES_PER_READ) {
65
+ return `Error (Read): Page range "${pages}" exceeds maximum of ${PDF_MAX_PAGES_PER_READ} pages per request. Please use a smaller range.`;
66
+ }
67
+ const pageCount = await getPdfPageCount(env, key, bytes, signal);
68
+ if (pageCount !== undefined && range.first > pageCount) {
69
+ return `Error (Read): page range "${pages}" starts past the end of the PDF (${pageCount} page${pageCount === 1 ? "" : "s"}).`;
70
+ }
71
+ let last = range.last === Infinity ? range.first + PDF_MAX_PAGES_PER_READ - 1 : range.last;
72
+ if (pageCount !== undefined)
73
+ last = Math.min(last, pageCount);
74
+ if (!cap.document && !cap.vision) {
75
+ const t = await extractPdfTextLayer(env, key, { first: range.first, last }, signal);
76
+ if (t.ok) {
77
+ const { body, truncated } = boundPdfExtractedText(t.text);
78
+ const timeoutNote = t.timedOutAfterSec !== undefined
79
+ ? `; PARTIAL — pdftotext timed out after ${t.timedOutAfterSec}s, so the text below covers only what was extracted before the timeout; use a narrower pages range for the rest`
80
+ : "";
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} extracted as text via pdftotext (the serving model does not support native PDF input)${timeoutNote}]` +
86
+ `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; narrow the pages range]` : ""}`,
87
+ },
88
+ ],
89
+ details: {
90
+ type: "document",
91
+ file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}`, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
92
+ fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
93
+ },
94
+ };
95
+ }
96
+ const why = t.toolMissing ? "the poppler utility `pdftotext` is not available in this environment" : `pdftotext failed: ${t.error}`;
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: `[PDF pages ${range.first}-${last} of ${path} omitted: the serving model supports neither native PDF (document) input nor image input, and engine-side text extraction did not run (${why}). ` +
102
+ `Install poppler-utils, or extract the text via Bash (e.g. \`pdftotext -layout -f ${range.first} -l ${last} "${path}" -\`).]`,
103
+ },
104
+ ],
105
+ details: {
106
+ type: "document",
107
+ file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}` },
108
+ fallback: { level: 3, reason: why },
109
+ },
110
+ };
111
+ }
112
+ const extract = await extractPdfPagesAsImages(env, key, range.first, last, signal);
113
+ if (!extract.ok) {
114
+ if (extract.toolMissing) {
115
+ return (`Error (Read): reading a PDF page range requires the poppler utility \`pdftoppm\`, which is not available in this environment. ` +
116
+ `Read the whole PDF without \`pages\` (works up to ${PDF_INLINE_PAGE_THRESHOLD} pages / 20 MB), or extract text via Bash (e.g. \`pdftotext\`).`);
117
+ }
118
+ return `Error (Read): PDF page extraction failed for "${path}": ${extract.error}`;
119
+ }
120
+ const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
121
+ if (typeof blocks === "string")
122
+ return blocks;
123
+ const requestedPages = last - range.first + 1;
124
+ const renderedAll = extract.pages.length >= requestedPages;
125
+ const header = extract.timedOutAfterSec !== undefined
126
+ ? renderedAll
127
+ ? `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""}: pdftoppm timed out after ${extract.timedOutAfterSec}s, but all ${requestedPages} requested page${requestedPages === 1 ? "" : "s"} were rendered before the timeout hit at teardown (100 dpi); nothing is missing.]`
128
+ : `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""}: pdftoppm timed out after ${extract.timedOutAfterSec}s; rendered ${extract.pages.length} of ${requestedPages} requested page${requestedPages === 1 ? "" : "s"} before the timeout (100 dpi). Read the remaining pages with a narrower range (e.g. pages: "${range.first + extract.pages.length}-${last}").]`
129
+ : `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} rendered as ${extract.pages.length} image${extract.pages.length === 1 ? "" : "s"} (100 dpi)]`;
130
+ const content = [
131
+ { type: "text", text: header },
132
+ ...blocks,
133
+ ];
134
+ return {
135
+ content,
136
+ details: {
137
+ type: "document",
138
+ file: {
139
+ type: "application/pdf",
140
+ originalSize: size,
141
+ ...(pageCount !== undefined ? { pageCount } : {}),
142
+ pages: `${range.first}-${last}`,
143
+ renderedPages: extract.pages.length,
144
+ ...(extract.timedOutAfterSec !== undefined ? { requestedPages, timedOut: true } : {}),
145
+ },
146
+ },
147
+ };
148
+ }
149
+ const pageCount = await getPdfPageCount(env, key, bytes, signal);
150
+ if (pageCount !== undefined && pageCount > PDF_INLINE_PAGE_THRESHOLD) {
151
+ return `Error (Read): This PDF has ${pageCount} 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.`;
152
+ }
153
+ if (size > PDF_TARGET_RAW_SIZE || bytes === undefined) {
154
+ return `Error (Read): PDF file exceeds maximum allowed size of 20.0 MB (${size} bytes). Use the pages parameter to read specific page ranges (e.g., pages: "1-5"), or extract text via Bash (e.g. \`pdftotext\`).`;
155
+ }
156
+ if (!cap.document) {
157
+ const fileCard = { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}) };
158
+ const t = await extractPdfTextLayer(env, key, undefined, signal);
159
+ if (t.ok) {
160
+ const { body, truncated } = boundPdfExtractedText(t.text);
161
+ const timeoutNote = t.timedOutAfterSec !== undefined
162
+ ? `; PARTIAL — pdftotext timed out after ${t.timedOutAfterSec}s, so the text below covers only what was extracted before the timeout; use the pages parameter for the rest`
163
+ : "";
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: `[PDF: ${path} — extracted text layer via pdftotext${pageCount !== undefined ? `; ${pageCount} page${pageCount === 1 ? "" : "s"}` : ""} (the serving model does not support native PDF input)${timeoutNote}]` +
169
+ `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; use the pages parameter to read specific ranges]` : ""}`,
170
+ },
171
+ ],
172
+ details: {
173
+ type: "document",
174
+ file: { ...fileCard, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
175
+ fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
176
+ },
177
+ };
178
+ }
179
+ const textWhy = t.toolMissing ? "`pdftotext` is not available in this environment" : `pdftotext failed: ${t.error}`;
180
+ let renderWhy;
181
+ if (cap.vision) {
182
+ const n = Math.min(PDF_FALLBACK_RENDER_PAGES, pageCount ?? PDF_FALLBACK_RENDER_PAGES);
183
+ const extract = await extractPdfPagesAsImages(env, key, 1, n, signal);
184
+ if (extract.ok) {
185
+ const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
186
+ if (typeof blocks !== "string") {
187
+ const renderTimeoutNote = extract.timedOutAfterSec !== undefined ? ` pdftoppm timed out after ${extract.timedOutAfterSec}s — only the pages that finished rendering are included.` : "";
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: `[PDF: ${path} — first ${extract.pages.length}${pageCount !== undefined ? ` of ${pageCount}` : ""} page${extract.pages.length === 1 ? "" : "s"} rendered as image${extract.pages.length === 1 ? "" : "s"} (100 dpi; ${textWhy}).${renderTimeoutNote} ` +
193
+ `Use the pages parameter (e.g., pages: "${extract.pages.length + 1}-${extract.pages.length + PDF_FALLBACK_RENDER_PAGES}") to read more pages.]`,
194
+ },
195
+ ...blocks,
196
+ ],
197
+ details: {
198
+ type: "document",
199
+ file: { ...fileCard, pages: `1-${extract.pages.length}`, renderedPages: extract.pages.length, ...(extract.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
200
+ fallback: { level: 2, reason: `model lacks document input; ${textWhy}; rendered first ${extract.pages.length} page(s) via pdftoppm` },
201
+ },
202
+ };
203
+ }
204
+ renderWhy = "a rendered page image exceeded the per-image API size limit";
205
+ }
206
+ else {
207
+ renderWhy = extract.toolMissing ? "the poppler utility `pdftoppm` is not available in this environment" : `pdftoppm failed: ${extract.error}`;
208
+ }
209
+ }
210
+ else {
211
+ renderWhy = "the serving model does not support image input, so pages cannot be rendered";
212
+ }
213
+ const why = `text extraction did not run (${textWhy}); page rendering did not run (${renderWhy})`;
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `[PDF document omitted: the serving model does not support native PDF (document) input, and engine-side fallbacks were unavailable — ${why}. ` +
219
+ `Install poppler-utils, or extract the text via Bash (e.g. \`pdftotext -layout "${path}" -\`).]`,
220
+ },
221
+ ],
222
+ details: { type: "document", file: fileCard, fallback: { level: 3, reason: why } },
223
+ };
224
+ }
225
+ const base64 = Buffer.from(bytes).toString("base64");
226
+ return {
227
+ content: [{ type: "document", data: base64, mimeType: "application/pdf" }],
228
+ details: {
229
+ type: "document",
230
+ file: { ...(base64.length <= 48_000 ? { base64 } : {}), type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}) },
231
+ },
232
+ };
233
+ }
234
+ export function pdfResultToToolReturn(r) {
235
+ return typeof r === "string" ? errorResult(r) : r;
236
+ }
@@ -0,0 +1,8 @@
1
+ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
+ import { type ReadFileState } from "./safety.js";
3
+ import { type PdfModelCapabilities } from "./pdf.js";
4
+ import { type ReadImageDownsamplerOption, type CwdRef } from "./fs-shared.js";
5
+ export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
6
+ taskId?: string;
7
+ principal?: string;
8
+ }) => boolean): AgentTool;
@@ -0,0 +1,286 @@
1
+ import { Type } from "typebox";
2
+ import { defineTool, errorResult } from "../../core/tools.js";
3
+ import { sha256, resolveKey, violationText, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
4
+ import { decodeTextBytes } from "./encoding.js";
5
+ import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
6
+ import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
7
+ import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
8
+ import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
9
+ import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
10
+ export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption) {
11
+ return defineTool({
12
+ name: "Read",
13
+ contract: { contractId: "core.read@1", implementationRevision: "1" },
14
+ description: "Reads a file from the local filesystem.\n" +
15
+ "\n" +
16
+ "- `file_path` may be relative (resolved against the tracked working directory) or absolute (within the configured roots).\n" +
17
+ "- By default, it reads the whole file (from `offset`, 1-based); a very large file is served as a partial view with an explicit marker and the next-page call.\n" +
18
+ "- When you already know which part of the file you need, only read that part. This can be important for larger files.\n" +
19
+ "- Results are returned using cat -n format, with line numbers starting at 1\n" +
20
+ "- Reads images (PNG, JPG, …) and presents them visually. Reads PDFs via the `pages` parameter (e.g. \"1-5\", max 20 pages/request; a PDF whose detected page count exceeds 10 requires `pages`). Reads Jupyter notebooks (.ipynb) as cells with outputs (offset/limit do not apply).\n" +
21
+ "- Reading a directory, a missing file, or an empty file returns an error or warning rather than content.\n" +
22
+ "- Other binaries (archives, executables) are refused. A whole-file read of a text file over 256 KB is refused — read it in slices with explicit offset/limit, or Grep it instead.\n" +
23
+ "- You must read a file before editing it.\n" +
24
+ "- Do NOT re-read a file you just edited to verify — Edit/Write would have errored if the change failed, and the harness tracks file state for you.",
25
+ descriptionClassic: "Reads a file from the local filesystem.\n" +
26
+ "\n" +
27
+ "Usage:\n" +
28
+ "- By default, it reads the whole file (from `offset`, 1-based); a very large file is served as a partial view with an explicit marker and the next-page call\n" +
29
+ "- When you already know which part of the file you need, only read that part. This can be important for larger files.\n" +
30
+ "- Results are returned using cat -n format, with each line prefixed by its line number and a tab\n" +
31
+ "- This tool allows the model to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually.\n" +
32
+ "- This tool can only read files, not directories. To read a directory, use Glob/Grep or an ls command via the Bash tool.\n" +
33
+ "- You must read a file before editing it.\n" +
34
+ "- Reads PDFs (.pdf): the document is provided to the model directly (text layer included). Use the `pages` parameter (e.g., \"1-5\") to read a page range; a PDF whose detected page count exceeds 10 requires `pages`. Maximum 20 pages per request.\n" +
35
+ "- Reads Jupyter notebooks (.ipynb) as cells with outputs (offset/limit do not apply).\n" +
36
+ "- Other binaries (archives, executables) are refused. A whole-file read of a text file over 256 KB is refused — read it in slices with explicit offset/limit, or Grep it instead.\n" +
37
+ "- If you read a file that exists but has empty contents you will receive a warning in place of file contents.\n" +
38
+ "- Do NOT re-read a file you just edited to verify — Edit/Write would have errored if the change failed, and the harness tracks file state for you.",
39
+ parameters: Type.Object({
40
+ ...FILE_PATH_PARAMS,
41
+ offset: Type.Optional(Type.Number({ description: "The line number to start reading from. Only provide if the file is too large to read at once (1-based; default 1)." })),
42
+ limit: Type.Optional(Type.Number({ description: "The number of lines to read. Only provide if the file is too large to read at once. (Default: the whole file, bounded by the output-token cap.)" })),
43
+ pages: Type.Optional(Type.String({ description: `Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.` })),
44
+ }),
45
+ effect: "read",
46
+ execute: async (args, ctx) => {
47
+ const { offset, limit, pages } = args;
48
+ const path = fileArgPath(args);
49
+ if (path === undefined)
50
+ return errorResult(`Error (Read): file_path is required.`);
51
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx));
52
+ if (!r.ok)
53
+ return errorResult(violationText("Read", r.violation));
54
+ const isNb = isNotebookPath(r.key);
55
+ const imageMime = imageMimeForRead(r.key);
56
+ if (imageMime !== undefined) {
57
+ const meta = await env.fileInfo(r.key, ctx.signal);
58
+ if (!meta.ok) {
59
+ const ex = await env.exists(r.key, ctx.signal);
60
+ if (ex.ok && !ex.value)
61
+ return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`);
62
+ return errorResult(`Error (Read): cannot stat image "${path}" to verify its size before reading: ${meta.error.message}`);
63
+ }
64
+ if (meta.value.kind === "directory") {
65
+ return errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`);
66
+ }
67
+ const downsampler = imageDownsampler === false ? undefined : (imageDownsampler ?? (await resolveAutoDownsampler()));
68
+ const readCap = downsampler ? MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES : MAX_IMAGE_READ_BYTES;
69
+ if (meta.value.size > readCap) {
70
+ return errorResult(`Error (Read): image "${path}" is too large to read (${meta.value.size} bytes > ${readCap}-byte cap).`);
71
+ }
72
+ const bin = await env.readBinaryFile(r.key, ctx.signal);
73
+ if (!bin.ok)
74
+ return errorResult(`Error (Read): cannot read image "${path}": ${bin.error.message}`);
75
+ if (bin.value.byteLength > readCap) {
76
+ return errorResult(`Error (Read): image "${path}" is too large to read (${bin.value.byteLength} bytes > ${readCap}-byte cap).`);
77
+ }
78
+ if (!imageMagicMatches(bin.value, imageMime)) {
79
+ return errorResult(`Error (Read): "${path}" has an image extension but its content is not a valid ${imageMime} (empty, truncated, or mis-named).`);
80
+ }
81
+ let base64;
82
+ let mime = imageMime;
83
+ let dimensions;
84
+ if (downsampler) {
85
+ const out = await downsampler(Buffer.from(bin.value), imageMime).catch(() => undefined);
86
+ if (out) {
87
+ base64 = out.base64;
88
+ mime = out.mimeType;
89
+ dimensions = out.dimensions;
90
+ }
91
+ }
92
+ base64 ??= Buffer.from(bin.value).toString("base64");
93
+ if (base64.length > MCP_IMAGE_MAX_BASE64) {
94
+ return errorResult(`Error (Read): image "${path}" is too large to send (${bin.value.byteLength} bytes; base64 ${base64.length} chars exceeds the ${MCP_IMAGE_MAX_BASE64}-char / 5MB API image limit` +
95
+ (downsampler
96
+ ? "; downsampling could not bring it under the limit)."
97
+ : `; raw images over ${IMAGE_TARGET_RAW_SIZE} bytes always exceed it — shrink it first, e.g. with an image tool via bash).`));
98
+ }
99
+ const wasResized = dimensions !== undefined && dimensions.displayWidth > 0 && dimensions.displayHeight > 0 && (dimensions.originalWidth !== dimensions.displayWidth || dimensions.originalHeight !== dimensions.displayHeight);
100
+ const content = [{ type: "image", data: base64, mimeType: mime }];
101
+ if (wasResized && dimensions) {
102
+ const scale = dimensions.originalWidth / dimensions.displayWidth;
103
+ content.push({
104
+ type: "text",
105
+ text: `[Image: original ${dimensions.originalWidth}x${dimensions.originalHeight}, displayed at ${dimensions.displayWidth}x${dimensions.displayHeight}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`,
106
+ });
107
+ }
108
+ return {
109
+ content,
110
+ details: {
111
+ type: "image",
112
+ file: { ...(base64.length <= 48_000 ? { base64 } : {}), type: mime, originalSize: bin.value.byteLength, ...(dimensions ? { dimensions } : {}) },
113
+ },
114
+ };
115
+ }
116
+ if (r.key.toLowerCase().endsWith(".pdf")) {
117
+ return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities));
118
+ }
119
+ if (hasBinaryExtension(r.key)) {
120
+ return errorResult(`Error (Read): "${path}" appears to be a binary file (by extension); this tool reads UTF-8 text only.`);
121
+ }
122
+ const info = await env.fileInfo(r.key, ctx.signal);
123
+ if (info.ok) {
124
+ if (info.value.kind === "directory") {
125
+ return errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`);
126
+ }
127
+ if (info.value.size > SLICED_READ_MAX_BYTES) {
128
+ return errorResult(`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
129
+ `(${info.value.size} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
130
+ `Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
131
+ }
132
+ if (!isNb && info.value.size > MAX_READ_BYTES && offset === undefined && limit === undefined) {
133
+ return errorResult(`Error (Read): "${path}" is too large to read in full (${info.value.size} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
134
+ }
135
+ }
136
+ else {
137
+ const ex = await env.exists(r.key, ctx.signal);
138
+ if (ex.ok && !ex.value)
139
+ return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`);
140
+ }
141
+ const readBin = await env.readBinaryFile(r.key, ctx.signal);
142
+ if (!readBin.ok)
143
+ return errorResult(`Error (Read): cannot read "${path}": ${readBin.error.message}`);
144
+ const readSize = readBin.value.byteLength;
145
+ if (readSize > SLICED_READ_MAX_BYTES) {
146
+ return errorResult(`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
147
+ `(${readSize} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
148
+ `Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
149
+ }
150
+ if (!isNb && readSize > MAX_READ_BYTES && offset === undefined && limit === undefined) {
151
+ return errorResult(`Error (Read): "${path}" is too large to read in full (${readSize} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
152
+ }
153
+ if (pdfMagicMatches(readBin.value)) {
154
+ return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities));
155
+ }
156
+ const decoded = decodeTextBytes(readBin.value);
157
+ if (decoded.malformed) {
158
+ return errorResult(`Error (Read): "${path}" has a UTF-16 BOM but a truncated (odd-length) body — the file is corrupt or mis-labelled; repair/convert it with bash (e.g. \`iconv\`) first.`);
159
+ }
160
+ const content = decoded.text;
161
+ if (isBinaryContent(content.slice(0, 4096))) {
162
+ return errorResult(`Error (Read): "${path}" appears to be a binary file (non-text content); this tool reads UTF-8 and BOM-marked UTF-16LE text only. ` +
163
+ `If it is UTF-16 without a BOM or a legacy encoding, convert it first (e.g. \`iconv -f UTF-16LE -t UTF-8\`) or inspect/transform it with bash.`);
164
+ }
165
+ const nbParsed = isNb ? parseNotebookCells(content, path) : undefined;
166
+ const nbForced = nbParsed !== undefined && nbParsed.ok;
167
+ const effOffset = nbForced ? undefined : offset;
168
+ const effLimit = nbForced ? undefined : limit;
169
+ const lines = content.length === 0 ? [] : content.split("\n");
170
+ const total = lines.length;
171
+ const start = Math.max(1, Math.floor(effOffset ?? 1));
172
+ const max = effLimit !== undefined ? Math.max(1, Math.floor(effLimit)) : Number.MAX_SAFE_INTEGER;
173
+ if (total > 0 && start > total) {
174
+ return `<system-reminder>Warning: the file exists but is shorter than the provided offset (${start}). The file has ${total} lines.</system-reminder>`;
175
+ }
176
+ let slice = lines.slice(start - 1, start - 1 + max);
177
+ let end = start - 1 + slice.length;
178
+ if (end === total - 1 && lines[total - 1] === "") {
179
+ slice = lines.slice(start - 1, total);
180
+ end = total;
181
+ }
182
+ const hash = sha256(content);
183
+ let nbFallbackPrefix = "";
184
+ if (nbParsed !== undefined) {
185
+ if (nbParsed.ok) {
186
+ const parsed = nbParsed;
187
+ const rendered = renderNotebookCells(parsed.cells, NOTEBOOK_IMAGE_BASE64_BUDGET);
188
+ if (rendered.textChars > MAX_READ_OUTPUT_CHARS) {
189
+ return errorResult(`Error (Read): notebook "${path}" projects to ~${Math.round(rendered.textChars / 4)} tokens of cell content, over the 25000-token limit ` +
190
+ `(offset/limit do not apply to notebooks — the whole notebook is always projected). Inspect portions with Bash + jq instead, e.g.\n` +
191
+ ` cat "${path}" | jq '.cells[:20]' # First 20 cells\n` +
192
+ ` cat "${path}" | jq '.cells | length' # Count total cells\n` +
193
+ ` cat "${path}" | jq '.cells[] | select(.cell_type=="code") | .source' # All code sources`);
194
+ }
195
+ const prevNb = state.get(r.key);
196
+ if (prevNb?.seededFromContext && prevNb.hash === hash) {
197
+ return seededFileUnchangedReminder(r.key);
198
+ }
199
+ if (prevNb && prevNb.hash === hash && prevNb.view && prevNb.view.start === 1 && prevNb.view.end === total) {
200
+ return `[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]`;
201
+ }
202
+ state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
203
+ const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
204
+ return {
205
+ content: [...bodyBlocks, { type: "text", text: READ_CYBER_REMINDER }],
206
+ details: { type: "notebook", file: { filePath: path, cells: parsed.cells.map(stripNotebookImageData) } },
207
+ };
208
+ }
209
+ nbFallbackPrefix = "[not a parseable notebook — showing raw file text]\n";
210
+ }
211
+ const renderBody = (sl) => sl.map((l, i) => `${start + i}\t${l}`).join("\n");
212
+ let body = renderBody(slice);
213
+ let pageMarker;
214
+ if (body.length > MAX_READ_OUTPUT_CHARS) {
215
+ const defaultFullRead = start <= 1 && effLimit === undefined;
216
+ if (!defaultFullRead) {
217
+ return errorResult(`Error (Read): the requested range of "${path}" is ~${Math.round(body.length / 4)} tokens, over the 25000-token limit. Narrow it with offset/limit, or use grep to search the file.`);
218
+ }
219
+ const approxTokens = Math.round(body.length / 4);
220
+ let n = Math.max(1, Math.min(slice.length, Math.floor(slice.length * (MAX_READ_OUTPUT_CHARS / body.length) * 0.95)));
221
+ let paged = slice.slice(0, n);
222
+ let pagedBody = renderBody(paged);
223
+ for (let attempt = 0; attempt < 8 && pagedBody.length > MAX_READ_OUTPUT_CHARS && n > 1; attempt++) {
224
+ n = Math.max(1, Math.floor(n * 0.7));
225
+ paged = slice.slice(0, n);
226
+ pagedBody = renderBody(paged);
227
+ }
228
+ if (pagedBody.length <= MAX_READ_OUTPUT_CHARS) {
229
+ slice = paged;
230
+ end = start - 1 + slice.length;
231
+ body = pagedBody;
232
+ pageMarker =
233
+ `[Truncated: PARTIAL view — showing lines 1-${end} of ${total} total (~${approxTokens} tokens, cap 25000). ` +
234
+ `Call Read with offset=${end + 1} limit=${end} for the next page, or Grep to find a specific section. ` +
235
+ `Do NOT answer from this page alone if the answer may be further in the file.]\n`;
236
+ }
237
+ else {
238
+ let cut = MAX_READ_OUTPUT_CHARS - 200;
239
+ const cc = slice[0].charCodeAt(cut - 1);
240
+ if (cc >= 0xd800 && cc <= 0xdbff)
241
+ cut -= 1;
242
+ slice = [slice[0].slice(0, cut)];
243
+ end = start;
244
+ body = renderBody(slice);
245
+ pageMarker =
246
+ `[Truncated: PARTIAL view — showing the first ${cut} of ${content.length} characters (~${approxTokens} tokens, cap 25000); ` +
247
+ `this file has very long lines and cannot be paginated by line. Use Grep to find a specific section, or Read with offset/limit to page through it. ` +
248
+ `Do NOT answer from this excerpt alone if the answer may be elsewhere in the file.]\n`;
249
+ }
250
+ }
251
+ const truncated = start > 1 || end < total || pageMarker !== undefined;
252
+ const prev = state.get(r.key);
253
+ if (total > 0 && prev?.seededFromContext && start === 1 && effLimit === undefined && prev.hash === hash) {
254
+ return seededFileUnchangedReminder(r.key);
255
+ }
256
+ if (total > 0 && prev && prev.hash === hash && prev.view && prev.view.start === start && prev.view.end === end) {
257
+ return `[${path}: unchanged since you last read it (lines ${start}-${end} of ${total}); content omitted to save context]`;
258
+ }
259
+ state.set(r.key, {
260
+ hash,
261
+ totalLines: countLines(content),
262
+ truncated,
263
+ ...(pageMarker !== undefined ? { isPartialView: true } : {}),
264
+ view: { start, end },
265
+ lastReadAt: Date.now(),
266
+ });
267
+ if (total === 0)
268
+ return `<system-reminder>Warning: the file exists but is shorter than the provided offset (${start}). The file has 1 lines.</system-reminder>`;
269
+ const header = pageMarker ?? (truncated ? `[${path}: lines ${start}-${end} of ${total}${end < total ? " — use offset to see more" : ""}]\n` : "");
270
+ return {
271
+ content: `${nbFallbackPrefix}${header}${body}${READ_CYBER_REMINDER}`,
272
+ details: {
273
+ type: "text",
274
+ file: {
275
+ filePath: path,
276
+ content: slice.join("\n"),
277
+ numLines: slice.length,
278
+ startLine: start,
279
+ totalLines: total,
280
+ ...(pageMarker !== undefined ? { truncatedByTokenCap: true } : {}),
281
+ },
282
+ },
283
+ };
284
+ },
285
+ });
286
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
+ import type { ToolEffect } from "../../core/types.js";
3
+ export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
4
+ export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
5
+ export declare const HAND_TOOL_EFFECTS: Readonly<Record<string, ToolEffect>>;