@sema-agent/core 1.279.0 → 1.280.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.
- package/dist/agents/subagent.d.ts +12 -0
- package/dist/agents/subagent.d.ts.map +1 -1
- package/dist/agents/subagent.js +221 -19
- package/dist/agents/subagent.js.map +1 -1
- package/dist/bin/sema-tb.d.ts +10 -0
- package/dist/bin/sema-tb.d.ts.map +1 -1
- package/dist/bin/sema-tb.js +81 -10
- package/dist/bin/sema-tb.js.map +1 -1
- package/dist/core/background-shell.d.ts +2 -0
- package/dist/core/background-shell.d.ts.map +1 -1
- package/dist/core/background-shell.js.map +1 -1
- package/dist/core/context-guard.d.ts +2 -0
- package/dist/core/context-guard.d.ts.map +1 -1
- package/dist/core/context-guard.js +10 -0
- package/dist/core/context-guard.js.map +1 -1
- package/dist/core/mcp.d.ts +4 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +189 -32
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/runner/call-cap.d.ts +4 -0
- package/dist/core/runner/call-cap.d.ts.map +1 -1
- package/dist/core/runner/call-cap.js +15 -1
- package/dist/core/runner/call-cap.js.map +1 -1
- package/dist/core/runner/prepare-task.d.ts.map +1 -1
- package/dist/core/runner/prepare-task.js +5 -3
- package/dist/core/runner/prepare-task.js.map +1 -1
- package/dist/core/runner/runtask.d.ts.map +1 -1
- package/dist/core/runner/runtask.js +28 -20
- package/dist/core/runner/runtask.js.map +1 -1
- package/dist/core/session-reconcile.d.ts +1 -1
- package/dist/core/session-reconcile.d.ts.map +1 -1
- package/dist/core/session-reconcile.js +7 -2
- package/dist/core/session-reconcile.js.map +1 -1
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-notification.d.ts.map +1 -1
- package/dist/core/task-notification.js.map +1 -1
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.d.ts.map +1 -1
- package/dist/core/task-registry.js +184 -55
- package/dist/core/task-registry.js.map +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts.map +1 -1
- package/dist/engine/execution-env/node-execution-env.js +17 -0
- package/dist/engine/execution-env/node-execution-env.js.map +1 -1
- package/dist/engine/harness/types.d.ts.map +1 -1
- package/dist/engine/harness/types.js.map +1 -1
- package/dist/engine/loop/agent-loop.js +15 -5
- package/dist/engine/loop/agent-loop.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestration/workflow.d.ts +19 -3
- package/dist/orchestration/workflow.d.ts.map +1 -1
- package/dist/orchestration/workflow.js +61 -11
- package/dist/orchestration/workflow.js.map +1 -1
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.d.ts.map +1 -1
- package/dist/tools/fs/index.js +137 -35
- package/dist/tools/fs/index.js.map +1 -1
- package/dist/tools/fs/pdf.d.ts +5 -0
- package/dist/tools/fs/pdf.d.ts.map +1 -1
- package/dist/tools/fs/pdf.js +58 -11
- package/dist/tools/fs/pdf.js.map +1 -1
- package/dist/tools/fs/safety.d.ts +1 -0
- package/dist/tools/fs/safety.d.ts.map +1 -1
- package/dist/tools/fs/safety.js +1 -0
- package/dist/tools/fs/safety.js.map +1 -1
- package/dist/tools/fs/search.d.ts +13 -0
- package/dist/tools/fs/search.d.ts.map +1 -1
- package/dist/tools/fs/search.js +58 -16
- package/dist/tools/fs/search.js.map +1 -1
- package/dist/tools/web.d.ts.map +1 -1
- package/dist/tools/web.js +141 -28
- package/dist/tools/web.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/fs/index.js
CHANGED
|
@@ -4,15 +4,21 @@ import { defineTool } from "../../core/tools.js";
|
|
|
4
4
|
import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../../core/task-registry.js";
|
|
5
5
|
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
6
6
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
7
|
-
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, similarNameSuggestion, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, } from "./safety.js";
|
|
7
|
+
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, similarNameSuggestion, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, OVERSIZE_READ_ESCAPE_HINT, } from "./safety.js";
|
|
8
8
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText } from "./encoding.js";
|
|
9
|
-
import {
|
|
9
|
+
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern } from "./search.js";
|
|
10
10
|
import { makeRepoMapTool } from "./repo-map.js";
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE, sharpImageDownsampler } from "../../core/mcp.js";
|
|
13
13
|
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";
|
|
14
14
|
export { pdfModelCapabilitiesOf } from "./pdf.js";
|
|
15
15
|
const MAX_READ_BYTES = 256 * 1024;
|
|
16
|
+
export const SLICED_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
async function notReadRefusalText(env, toolName, key, v, signal) {
|
|
18
|
+
const base = `Error (${toolName}): ${v.message}`;
|
|
19
|
+
const info = await env.fileInfo(key, signal);
|
|
20
|
+
return info.ok && info.value.kind !== "directory" && info.value.size > MAX_READ_BYTES ? `${base} ${OVERSIZE_READ_ESCAPE_HINT}` : base;
|
|
21
|
+
}
|
|
16
22
|
const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024;
|
|
17
23
|
const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES = 64 * 1024 * 1024;
|
|
18
24
|
let autoDownsampler;
|
|
@@ -162,17 +168,20 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
|
|
|
162
168
|
const t = await extractPdfTextLayer(env, key, { first: range.first, last }, signal);
|
|
163
169
|
if (t.ok) {
|
|
164
170
|
const { body, truncated } = boundPdfExtractedText(t.text);
|
|
171
|
+
const timeoutNote = t.timedOutAfterSec !== undefined
|
|
172
|
+
? `; 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`
|
|
173
|
+
: "";
|
|
165
174
|
return {
|
|
166
175
|
content: [
|
|
167
176
|
{
|
|
168
177
|
type: "text",
|
|
169
|
-
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)]` +
|
|
178
|
+
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}]` +
|
|
170
179
|
`\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; narrow the pages range]` : ""}`,
|
|
171
180
|
},
|
|
172
181
|
],
|
|
173
182
|
details: {
|
|
174
183
|
type: "document",
|
|
175
|
-
file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}
|
|
184
|
+
file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}`, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
|
|
176
185
|
fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
|
|
177
186
|
},
|
|
178
187
|
};
|
|
@@ -204,18 +213,29 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
|
|
|
204
213
|
const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
|
|
205
214
|
if (typeof blocks === "string")
|
|
206
215
|
return blocks;
|
|
216
|
+
const requestedPages = last - range.first + 1;
|
|
217
|
+
const renderedAll = extract.pages.length >= requestedPages;
|
|
218
|
+
const header = extract.timedOutAfterSec !== undefined
|
|
219
|
+
? renderedAll
|
|
220
|
+
? `[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.]`
|
|
221
|
+
: `[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}").]`
|
|
222
|
+
: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} rendered as ${extract.pages.length} image${extract.pages.length === 1 ? "" : "s"} (100 dpi)]`;
|
|
207
223
|
const content = [
|
|
208
|
-
{
|
|
209
|
-
type: "text",
|
|
210
|
-
text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} rendered as ${extract.pages.length} image${extract.pages.length === 1 ? "" : "s"} (100 dpi)]`,
|
|
211
|
-
},
|
|
224
|
+
{ type: "text", text: header },
|
|
212
225
|
...blocks,
|
|
213
226
|
];
|
|
214
227
|
return {
|
|
215
228
|
content,
|
|
216
229
|
details: {
|
|
217
230
|
type: "document",
|
|
218
|
-
file: {
|
|
231
|
+
file: {
|
|
232
|
+
type: "application/pdf",
|
|
233
|
+
originalSize: size,
|
|
234
|
+
...(pageCount !== undefined ? { pageCount } : {}),
|
|
235
|
+
pages: `${range.first}-${last}`,
|
|
236
|
+
renderedPages: extract.pages.length,
|
|
237
|
+
...(extract.timedOutAfterSec !== undefined ? { requestedPages, timedOut: true } : {}),
|
|
238
|
+
},
|
|
219
239
|
},
|
|
220
240
|
};
|
|
221
241
|
}
|
|
@@ -231,15 +251,22 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
|
|
|
231
251
|
const t = await extractPdfTextLayer(env, key, undefined, signal);
|
|
232
252
|
if (t.ok) {
|
|
233
253
|
const { body, truncated } = boundPdfExtractedText(t.text);
|
|
254
|
+
const timeoutNote = t.timedOutAfterSec !== undefined
|
|
255
|
+
? `; 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`
|
|
256
|
+
: "";
|
|
234
257
|
return {
|
|
235
258
|
content: [
|
|
236
259
|
{
|
|
237
260
|
type: "text",
|
|
238
|
-
text: `[PDF: ${path} — extracted text layer via pdftotext${pageCount !== undefined ? `; ${pageCount} page${pageCount === 1 ? "" : "s"}` : ""} (the serving model does not support native PDF input)]` +
|
|
261
|
+
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}]` +
|
|
239
262
|
`\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; use the pages parameter to read specific ranges]` : ""}`,
|
|
240
263
|
},
|
|
241
264
|
],
|
|
242
|
-
details: {
|
|
265
|
+
details: {
|
|
266
|
+
type: "document",
|
|
267
|
+
file: { ...fileCard, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
|
|
268
|
+
fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
|
|
269
|
+
},
|
|
243
270
|
};
|
|
244
271
|
}
|
|
245
272
|
const textWhy = t.toolMissing ? "`pdftotext` is not available in this environment" : `pdftotext failed: ${t.error}`;
|
|
@@ -250,18 +277,19 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
|
|
|
250
277
|
if (extract.ok) {
|
|
251
278
|
const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
|
|
252
279
|
if (typeof blocks !== "string") {
|
|
280
|
+
const renderTimeoutNote = extract.timedOutAfterSec !== undefined ? ` pdftoppm timed out after ${extract.timedOutAfterSec}s — only the pages that finished rendering are included.` : "";
|
|
253
281
|
return {
|
|
254
282
|
content: [
|
|
255
283
|
{
|
|
256
284
|
type: "text",
|
|
257
|
-
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})
|
|
258
|
-
`Use the pages parameter (e.g., pages: "${
|
|
285
|
+
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} ` +
|
|
286
|
+
`Use the pages parameter (e.g., pages: "${extract.pages.length + 1}-${extract.pages.length + PDF_FALLBACK_RENDER_PAGES}") to read more pages.]`,
|
|
259
287
|
},
|
|
260
288
|
...blocks,
|
|
261
289
|
],
|
|
262
290
|
details: {
|
|
263
291
|
type: "document",
|
|
264
|
-
file: { ...fileCard, pages: `1-${extract.pages.length}`, renderedPages: extract.pages.length },
|
|
292
|
+
file: { ...fileCard, pages: `1-${extract.pages.length}`, renderedPages: extract.pages.length, ...(extract.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
|
|
265
293
|
fallback: { level: 2, reason: `model lacks document input; ${textWhy}; rendered first ${extract.pages.length} page(s) via pdftoppm` },
|
|
266
294
|
},
|
|
267
295
|
};
|
|
@@ -309,7 +337,7 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
|
|
|
309
337
|
"- This tool can only read files, not directories. To read a directory, use Glob/Grep or an ls command via the Bash tool.\n" +
|
|
310
338
|
"- You must read a file before editing it.\n" +
|
|
311
339
|
"- 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; PDFs over 10 pages REQUIRE `pages`. Maximum 20 pages per request.\n" +
|
|
312
|
-
"- Other binaries (archives, executables)
|
|
340
|
+
"- 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" +
|
|
313
341
|
"- If you read a file that exists but has empty contents you will receive a warning in place of file contents.\n" +
|
|
314
342
|
"- 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.",
|
|
315
343
|
parameters: Type.Object({
|
|
@@ -399,8 +427,13 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
|
|
|
399
427
|
if (info.value.kind === "directory") {
|
|
400
428
|
return `Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`;
|
|
401
429
|
}
|
|
402
|
-
if (info.value.size >
|
|
403
|
-
return `Error (Read): "${path}" is too large to read
|
|
430
|
+
if (info.value.size > SLICED_READ_MAX_BYTES) {
|
|
431
|
+
return (`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
|
|
432
|
+
`(${info.value.size} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
|
|
433
|
+
`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.`);
|
|
434
|
+
}
|
|
435
|
+
if (info.value.size > MAX_READ_BYTES && offset === undefined && limit === undefined) {
|
|
436
|
+
return `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.`;
|
|
404
437
|
}
|
|
405
438
|
}
|
|
406
439
|
else {
|
|
@@ -411,6 +444,15 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
|
|
|
411
444
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
412
445
|
if (!readBin.ok)
|
|
413
446
|
return `Error (Read): cannot read "${path}": ${readBin.error.message}`;
|
|
447
|
+
const readSize = readBin.value.byteLength;
|
|
448
|
+
if (readSize > SLICED_READ_MAX_BYTES) {
|
|
449
|
+
return (`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
|
|
450
|
+
`(${readSize} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
|
|
451
|
+
`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.`);
|
|
452
|
+
}
|
|
453
|
+
if (readSize > MAX_READ_BYTES && offset === undefined && limit === undefined) {
|
|
454
|
+
return `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.`;
|
|
455
|
+
}
|
|
414
456
|
if (pdfMagicMatches(readBin.value)) {
|
|
415
457
|
return await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities);
|
|
416
458
|
}
|
|
@@ -617,7 +659,7 @@ export function makeEditFileTool(env, state, rootCanonical, cwdRef, additionalRo
|
|
|
617
659
|
}
|
|
618
660
|
const notRead = requireRead(state, r.key);
|
|
619
661
|
if (notRead)
|
|
620
|
-
return
|
|
662
|
+
return await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal);
|
|
621
663
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
622
664
|
if (!readBin.ok)
|
|
623
665
|
return `Error (Edit): cannot read "${path}": ${readBin.error.message}`;
|
|
@@ -795,7 +837,7 @@ export function makeNotebookEditTool(env, state, rootCanonical, cwdRef, addition
|
|
|
795
837
|
return violationText("NotebookEdit", r.violation);
|
|
796
838
|
const notRead = requireRead(state, r.key);
|
|
797
839
|
if (notRead)
|
|
798
|
-
return
|
|
840
|
+
return await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal);
|
|
799
841
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
800
842
|
if (!readBin.ok)
|
|
801
843
|
return `Error (NotebookEdit): cannot read "${notebook_path}": ${readBin.error.message}`;
|
|
@@ -936,7 +978,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
936
978
|
return violationText("Grep", r.violation);
|
|
937
979
|
scoped = r.key;
|
|
938
980
|
}
|
|
939
|
-
const
|
|
981
|
+
const grepRun = await runGrepDetailed(env, rootCanonical, {
|
|
940
982
|
...a,
|
|
941
983
|
path: scoped,
|
|
942
984
|
head_limit: a.head_limit ?? a.max_results,
|
|
@@ -946,6 +988,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
946
988
|
ignore_case: a.ignore_case ?? a["-i"],
|
|
947
989
|
only_matching: a.only_matching ?? a["-o"],
|
|
948
990
|
}, ctx.signal);
|
|
991
|
+
const text = grepRun.text;
|
|
949
992
|
const mode = a.output_mode ?? "content";
|
|
950
993
|
const rows = text.startsWith("No matches.")
|
|
951
994
|
? []
|
|
@@ -984,7 +1027,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
984
1027
|
}
|
|
985
1028
|
return {
|
|
986
1029
|
content: text,
|
|
987
|
-
details: { type: "grep", mode, ...detailFields },
|
|
1030
|
+
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}) },
|
|
988
1031
|
};
|
|
989
1032
|
},
|
|
990
1033
|
});
|
|
@@ -1206,7 +1249,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1206
1249
|
if (res.error.code === "transport_lost") {
|
|
1207
1250
|
return `Error (${toolName}): connection to the execution environment was lost — the command's outcome is unknown and it may still be running. Verify its effects before retrying; retry only if the command is idempotent. (${res.error.message})`;
|
|
1208
1251
|
}
|
|
1209
|
-
if (res.error.code === "timeout" || res.error.code === "aborted") {
|
|
1252
|
+
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
1210
1253
|
const clip = clipShellOutput;
|
|
1211
1254
|
const pStdout = clip(res.error.partialStdout ?? "");
|
|
1212
1255
|
const pStderr = clip(res.error.partialStderr ?? "");
|
|
@@ -1216,7 +1259,9 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1216
1259
|
? `Command was cut off after ${timeout}s — NOT its requested ${requestedSec}s timeout: the task's wall-clock deadline has already been reached, so the command only got a ${timeout}s grace window. The process was killed; output produced before the cutoff is shown below. Do NOT retry this foreground command — there is no time left. Use run_in_background (exempt from this clamp) if the work must continue, or write out your results now.`
|
|
1217
1260
|
: `Command was cut off after ${timeout}s — NOT its requested ${requestedSec}s timeout: the task is near its wall-clock deadline, so the foreground time budget was clamped to the ${timeout}s remaining. The process was killed; output produced before the cutoff is shown below. Do NOT retry this foreground command — the next attempt gets even less time. Use run_in_background (exempt from this clamp) if the work must continue, or write out your results now.`
|
|
1218
1261
|
: `Command timed out after ${timeout}s (timeout limit: ${timeout}s). The process was killed; output produced before the cutoff is shown below. Re-running the same command will likely time out again — consider run_in_background for long commands, a narrower command, or resuming from the partial progress below.`
|
|
1219
|
-
:
|
|
1262
|
+
: res.error.code === "aborted"
|
|
1263
|
+
? `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`
|
|
1264
|
+
: `Command started but its completion could not be observed — a host-side output-stream callback failed mid-run and the process was terminated (${res.error.message}). Output captured before the cut is shown below; its effects up to that point may have landed. Verify before assuming it needs a full re-run.`;
|
|
1220
1265
|
const body = (pStdout ? `\n--- partial stdout ---\n${pStdout}` : "") +
|
|
1221
1266
|
(pStderr ? `\n--- partial stderr ---\n${pStderr}` : "") +
|
|
1222
1267
|
(!pStdout && !pStderr ? `\n(no output was produced before the cutoff)` : "");
|
|
@@ -1233,14 +1278,16 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1233
1278
|
timeoutSec: timeout,
|
|
1234
1279
|
...(clampedByDeadline ? { requestedTimeoutSec: requestedSec, clampedByDeadline: true } : {}),
|
|
1235
1280
|
}
|
|
1236
|
-
:
|
|
1281
|
+
: res.error.code === "aborted"
|
|
1282
|
+
? { aborted: true }
|
|
1283
|
+
: { callbackError: true }),
|
|
1237
1284
|
},
|
|
1238
1285
|
};
|
|
1239
1286
|
}
|
|
1240
|
-
return `Error (${toolName}): command
|
|
1287
|
+
return `Error (${toolName}): command execution failed (${res.error.code}): ${res.error.message}. Whether the command started or completed could not be determined — verify its effects before retrying.`;
|
|
1241
1288
|
}
|
|
1242
1289
|
if (res.value.detached && detach) {
|
|
1243
|
-
return detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr);
|
|
1290
|
+
return await detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr);
|
|
1244
1291
|
}
|
|
1245
1292
|
let { stderr } = res.value;
|
|
1246
1293
|
const { stdout, exitCode } = res.value;
|
|
@@ -1349,12 +1396,18 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
|
|
|
1349
1396
|
if (diagnosis !== undefined)
|
|
1350
1397
|
throw new Error(diagnosis);
|
|
1351
1398
|
}
|
|
1399
|
+
const requestedTimeoutSec = timeout !== undefined ? Math.max(1, Math.round(timeout / 1000)) : undefined;
|
|
1352
1400
|
const r = await env.spawnBackground(command, {
|
|
1353
1401
|
cwd: cwdRef.current,
|
|
1354
|
-
...(
|
|
1402
|
+
...(requestedTimeoutSec !== undefined ? { timeout: requestedTimeoutSec } : {}),
|
|
1355
1403
|
});
|
|
1356
1404
|
if (!r.ok)
|
|
1357
1405
|
return `Error (Bash): ${r.error.message}`;
|
|
1406
|
+
const bgCaps = env.backgroundCapabilities;
|
|
1407
|
+
const appliedTimeoutSec = typeof bgCaps.defaultBgTimeoutSec === "number" && typeof bgCaps.maxBgTimeoutSec === "number"
|
|
1408
|
+
? Math.min(requestedTimeoutSec ?? bgCaps.defaultBgTimeoutSec, bgCaps.maxBgTimeoutSec)
|
|
1409
|
+
: undefined;
|
|
1410
|
+
const budgetNote = appliedTimeoutSec !== undefined ? ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).` : "";
|
|
1358
1411
|
if (taskOpts.taskRegistry) {
|
|
1359
1412
|
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
1360
1413
|
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
@@ -1374,24 +1427,43 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
|
|
|
1374
1427
|
});
|
|
1375
1428
|
}
|
|
1376
1429
|
catch (e) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1430
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1431
|
+
let kill;
|
|
1432
|
+
try {
|
|
1433
|
+
kill = await env.killBackground(r.value.shellId);
|
|
1434
|
+
}
|
|
1435
|
+
catch {
|
|
1436
|
+
kill = undefined;
|
|
1437
|
+
}
|
|
1438
|
+
if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
|
|
1439
|
+
return `Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`;
|
|
1440
|
+
}
|
|
1441
|
+
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
1442
|
+
return {
|
|
1443
|
+
content: `Error (Bash): background process was started but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
|
|
1444
|
+
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
1445
|
+
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
|
|
1446
|
+
details: { type: "bash", registerFailed: true, killFailed: true },
|
|
1447
|
+
};
|
|
1379
1448
|
}
|
|
1380
1449
|
return onNotify !== undefined
|
|
1381
1450
|
? `Command running in background; task_id=${taskId}. You will be notified when it completes — do not poll; ` +
|
|
1382
|
-
`end your turn if nothing else is pending. Use TaskOutput("${taskId}") to check interim output; TaskStop("${taskId}") to stop.`
|
|
1451
|
+
`end your turn if nothing else is pending. Use TaskOutput("${taskId}") to check interim output; TaskStop("${taskId}") to stop.` +
|
|
1452
|
+
budgetNote
|
|
1383
1453
|
: `Started in background; task_id=${taskId}. ` +
|
|
1384
1454
|
`Use TaskOutput("${taskId}") to read output (poll until status is no longer "running"); ` +
|
|
1385
|
-
`TaskStop("${taskId}") to stop. No '&' needed
|
|
1455
|
+
`TaskStop("${taskId}") to stop. No '&' needed.` +
|
|
1456
|
+
budgetNote;
|
|
1386
1457
|
}
|
|
1387
1458
|
return (`Started in background; task_id=${r.value.shellId}. Legacy bash_id=${r.value.shellId}. ` +
|
|
1388
1459
|
`Use TaskOutput("${r.value.shellId}") to read output (poll until status is no longer "running"); ` +
|
|
1389
|
-
`TaskStop("${r.value.shellId}") to stop. No '&' needed.`
|
|
1460
|
+
`TaskStop("${r.value.shellId}") to stop. No '&' needed.` +
|
|
1461
|
+
budgetNote);
|
|
1390
1462
|
}
|
|
1391
1463
|
const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
|
|
1392
1464
|
? {
|
|
1393
1465
|
signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
|
|
1394
|
-
onDetached: (shellId, stdoutSoFar,
|
|
1466
|
+
onDetached: async (shellId, stdoutSoFar, stderrSoFar) => {
|
|
1395
1467
|
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
1396
1468
|
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
1397
1469
|
const scope = ctx.principal ?? taskOpts.taskScope;
|
|
@@ -1410,18 +1482,48 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
|
|
|
1410
1482
|
});
|
|
1411
1483
|
}
|
|
1412
1484
|
catch (e) {
|
|
1485
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1486
|
+
let kill;
|
|
1487
|
+
try {
|
|
1488
|
+
kill = await env.killBackground(shellId);
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
kill = undefined;
|
|
1492
|
+
}
|
|
1493
|
+
const killed = kill !== undefined && (kill.ok || kill.error.code === "not_found");
|
|
1494
|
+
const outTail = stdoutSoFar.slice(-1_000);
|
|
1495
|
+
const errTail = stderrSoFar.slice(-1_000);
|
|
1496
|
+
const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
|
|
1497
|
+
(errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
|
|
1498
|
+
if (killed) {
|
|
1499
|
+
return {
|
|
1500
|
+
content: `Error (Bash): command was detached to the background but could not be registered (${reason}); ` +
|
|
1501
|
+
`it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
|
|
1502
|
+
tails,
|
|
1503
|
+
details: { type: "bash", detached: true, registerFailed: true },
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
1413
1507
|
return {
|
|
1414
|
-
content: `
|
|
1415
|
-
|
|
1508
|
+
content: `Error (Bash): command was detached to the background but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
|
|
1509
|
+
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
1510
|
+
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
|
|
1511
|
+
tails,
|
|
1512
|
+
details: { type: "bash", detached: true, registerFailed: true, killFailed: true },
|
|
1416
1513
|
};
|
|
1417
1514
|
}
|
|
1418
1515
|
const tail = stdoutSoFar.slice(-1_000);
|
|
1516
|
+
const adoptCaps = env.backgroundCapabilities;
|
|
1517
|
+
const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
|
|
1518
|
+
? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
|
|
1519
|
+
: "";
|
|
1419
1520
|
return {
|
|
1420
1521
|
content: `Command moved to background; task_id=${taskId}. ` +
|
|
1421
1522
|
`Use TaskOutput("${taskId}") to read its output; TaskStop("${taskId}") to stop it. ` +
|
|
1422
1523
|
(onNotify !== undefined
|
|
1423
1524
|
? `You will be notified when it completes — do not poll.`
|
|
1424
1525
|
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
1526
|
+
adoptBudgetNote +
|
|
1425
1527
|
(tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
|
|
1426
1528
|
details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}) },
|
|
1427
1529
|
};
|