@sema-agent/core 1.434.0 → 1.436.2
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 +1 -0
- package/dist/agents/subagent.js +24 -6
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/task-registry.d.ts +3 -0
- package/dist/core/task-registry.js +12 -23
- package/dist/core/tool-errors.d.ts +1 -0
- package/dist/core/tool-errors.js +21 -0
- package/dist/tools/fs/index.js +50 -20
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { type ExecutionEnv } from "../internal/harness.js";
|
|
|
5
5
|
import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
6
6
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
7
7
|
export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
|
|
8
|
+
export declare function notifyResultField(result: string | undefined): string | undefined;
|
|
8
9
|
export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
|
|
9
10
|
export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
|
|
10
11
|
export declare const LEGACY_SUBAGENT_TOOL_NAME = "Task";
|
package/dist/agents/subagent.js
CHANGED
|
@@ -12,6 +12,7 @@ import { SUBAGENT_PROMPT } from "../prompts/default.js";
|
|
|
12
12
|
import { hasSessionFork } from "../core/session.js";
|
|
13
13
|
import { isDurablePause } from "./suspend-guard.js";
|
|
14
14
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
15
|
+
import { boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
15
16
|
import { uuidv7 } from "../internal/harness.js";
|
|
16
17
|
import { addWorktree } from "../core/git-worktree-env.js";
|
|
17
18
|
import { BG_AGENT_REAP_STOP_ERROR, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS, normalizeAgentName } from "../core/task-registry.js";
|
|
@@ -19,6 +20,23 @@ import { canAccessAgentRecord } from "../core/background-agent-store.js";
|
|
|
19
20
|
import { extractErrorCode } from "../brain/errors.js";
|
|
20
21
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, observerFramingPrompt, observerSlug, resolveObserverDeclaration, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
21
22
|
import { SubagentStepRecorder, stepsFromMessages } from "./subagent-steps.js";
|
|
23
|
+
const BG_AGENT_RESULT_MAX = 4_000;
|
|
24
|
+
const BG_AGENT_RESULT_FULL_MAX = 200_000;
|
|
25
|
+
function resultSettleFields(result) {
|
|
26
|
+
if (!result)
|
|
27
|
+
return {};
|
|
28
|
+
const full = boundedRedactedSummary(result, BG_AGENT_RESULT_FULL_MAX);
|
|
29
|
+
const display = full.length > BG_AGENT_RESULT_MAX ? boundedRedactedSummary(result, BG_AGENT_RESULT_MAX) : full;
|
|
30
|
+
return display === full ? { result: display } : { result: display, resultFull: full };
|
|
31
|
+
}
|
|
32
|
+
const BG_AGENT_NOTIFY_RESULT_MAX = 2_000;
|
|
33
|
+
export function notifyResultField(result) {
|
|
34
|
+
if (!result)
|
|
35
|
+
return undefined;
|
|
36
|
+
return result.length > BG_AGENT_NOTIFY_RESULT_MAX
|
|
37
|
+
? `${result.slice(0, BG_AGENT_NOTIFY_RESULT_MAX)}\n[result truncated: ${result.length} chars total — call TaskOutput for the full text]`
|
|
38
|
+
: result;
|
|
39
|
+
}
|
|
22
40
|
export function inheritedManifestScopeFor(snapshot) {
|
|
23
41
|
if (!snapshot || snapshot.length === 0)
|
|
24
42
|
return undefined;
|
|
@@ -610,7 +628,7 @@ function makeSubagentResume(deps) {
|
|
|
610
628
|
try {
|
|
611
629
|
const winner = deps.registry.settleRevivedAgent(deps.taskId, reviveCycle, {
|
|
612
630
|
status,
|
|
613
|
-
...(child.result
|
|
631
|
+
...resultSettleFields(child.result),
|
|
614
632
|
...(status !== "completed" ? { error: (child.errorMessage ?? `resumed run ${status}`).slice(0, 500) } : {}),
|
|
615
633
|
});
|
|
616
634
|
if (winner !== undefined)
|
|
@@ -644,7 +662,7 @@ function makeSubagentResume(deps) {
|
|
|
644
662
|
...(stoppedByRevive !== undefined ? { stoppedBy: stoppedByRevive } : {}),
|
|
645
663
|
seq: entry.cycleSeq,
|
|
646
664
|
summary: `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`,
|
|
647
|
-
...(child.result ? { result: child.result
|
|
665
|
+
...(child.result ? { result: notifyResultField(child.result) } : {}),
|
|
648
666
|
...(status === "killed" && child.result ? { partial: true } : {}),
|
|
649
667
|
...resumeResidual(),
|
|
650
668
|
resumable: status !== "killed",
|
|
@@ -1771,7 +1789,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
|
|
|
1771
1789
|
const reapedBg = !okBg && abort.signal.aborted;
|
|
1772
1790
|
const settledBg = bg.registry.settleBackgroundAgent(taskId, {
|
|
1773
1791
|
status: okBg ? "completed" : reapedBg ? "killed" : "failed",
|
|
1774
|
-
...(child.result
|
|
1792
|
+
...resultSettleFields(child.result),
|
|
1775
1793
|
...(!okBg ? { error: reapedBg ? BG_AGENT_REAP_STOP_ERROR : child.errorMessage ?? String(child.status) } : {}),
|
|
1776
1794
|
}) ?? (abort.signal.aborted ? "killed" : okBg ? "completed" : "failed");
|
|
1777
1795
|
const stoppedByBg = settledBg === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
|
|
@@ -1809,7 +1827,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
|
|
|
1809
1827
|
...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
|
|
1810
1828
|
summary: `${ccCompletionText(shortDesc, settledBg, String(child.status), Date.now() - forkBgStartedAt)}`,
|
|
1811
1829
|
...(child.sessionId ? { sessionId: child.sessionId } : {}),
|
|
1812
|
-
...(child.result ? { result: child.result
|
|
1830
|
+
...(child.result ? { result: notifyResultField(child.result) } : {}),
|
|
1813
1831
|
...(settledBg === "killed" && child.result ? { partial: true } : {}),
|
|
1814
1832
|
...residualFork,
|
|
1815
1833
|
resumable: resumableFork,
|
|
@@ -2347,7 +2365,7 @@ task_id: ${taskId}
|
|
|
2347
2365
|
const settled = bg.registry.settleBackgroundAgent(taskId, {
|
|
2348
2366
|
status: ok ? "completed" : reaped ? "killed" : "failed",
|
|
2349
2367
|
seq: seqAtSettle ?? 1,
|
|
2350
|
-
...(child.result
|
|
2368
|
+
...resultSettleFields(child.result),
|
|
2351
2369
|
...(!ok ? { error: reaped ? BG_AGENT_REAP_STOP_ERROR : child.errorMessage ?? String(child.status) } : {}),
|
|
2352
2370
|
}) ??
|
|
2353
2371
|
(abort.signal.aborted ? "killed" : ok ? "completed" : "failed");
|
|
@@ -2386,7 +2404,7 @@ task_id: ${taskId}
|
|
|
2386
2404
|
...(stoppedBy !== undefined ? { stoppedBy } : {}),
|
|
2387
2405
|
summary: `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`,
|
|
2388
2406
|
...(child.sessionId ? { sessionId: child.sessionId } : {}),
|
|
2389
|
-
...(child.result ? { result: child.result
|
|
2407
|
+
...(child.result ? { result: notifyResultField(child.result) } : {}),
|
|
2390
2408
|
...(settled === "killed" && child.result ? { partial: true } : {}),
|
|
2391
2409
|
...residual,
|
|
2392
2410
|
resumable: resumableBg,
|
|
@@ -97,6 +97,7 @@ interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
97
97
|
name?: string;
|
|
98
98
|
sessionScoped?: true;
|
|
99
99
|
result?: string;
|
|
100
|
+
resultFull?: string;
|
|
100
101
|
error?: string;
|
|
101
102
|
resultIsPartial?: boolean;
|
|
102
103
|
stopSource?: StopSource;
|
|
@@ -291,6 +292,7 @@ export declare class TaskRegistry {
|
|
|
291
292
|
settleBackgroundAgent(id: string, outcome: {
|
|
292
293
|
status: "completed" | "failed" | "killed";
|
|
293
294
|
result?: string;
|
|
295
|
+
resultFull?: string;
|
|
294
296
|
error?: string;
|
|
295
297
|
stoppedBy?: StopSource;
|
|
296
298
|
seq?: number;
|
|
@@ -353,6 +355,7 @@ export declare class TaskRegistry {
|
|
|
353
355
|
settleRevivedAgent(id: string, cycle: number, outcome: {
|
|
354
356
|
status: "completed" | "failed" | "killed";
|
|
355
357
|
result?: string;
|
|
358
|
+
resultFull?: string;
|
|
356
359
|
error?: string;
|
|
357
360
|
}): "completed" | "failed" | "killed" | undefined;
|
|
358
361
|
unmarkRetainedContinuation(id: string): void;
|
|
@@ -7,6 +7,7 @@ import { summarizeWorkflowRun } from "./workflow-run-store.js";
|
|
|
7
7
|
import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
|
|
8
8
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
9
9
|
import { boundedRedactedSummary } from "./untrusted-egress.js";
|
|
10
|
+
import { clipWithFilePointer } from "./tool-errors.js";
|
|
10
11
|
const defaultMonitorTimers = {
|
|
11
12
|
setInterval: (fn, ms) => {
|
|
12
13
|
const h = setInterval(() => void fn(), ms);
|
|
@@ -52,8 +53,6 @@ const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
|
|
|
52
53
|
const TASK_OUTPUT_MAX_CHARS = 160_000;
|
|
53
54
|
const TASK_OUTPUT_ALIASES = ["BashOutput", "AgentOutputTool", "BashOutputTool", "AgentOutput", "WorkflowStatus"];
|
|
54
55
|
const TASK_STOP_ALIASES = ["KillShell", "KillBash"];
|
|
55
|
-
const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
|
|
56
|
-
const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
|
|
57
56
|
const TASK_OUTPUT_MIN_CHARS = 512;
|
|
58
57
|
function taskMaxOutputChars() {
|
|
59
58
|
const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
|
|
@@ -67,24 +66,7 @@ function taskMaxOutputChars() {
|
|
|
67
66
|
return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
|
|
68
67
|
}
|
|
69
68
|
export function clipTaskOutput(s, fullOutputPath) {
|
|
70
|
-
|
|
71
|
-
if (s.length <= limit)
|
|
72
|
-
return s;
|
|
73
|
-
if (fullOutputPath !== undefined) {
|
|
74
|
-
const header = `[Truncated. Full output: ${fullOutputPath}]\n\n`;
|
|
75
|
-
const room = limit - header.length;
|
|
76
|
-
return room > 0 ? header + s.slice(-room) : header.trimEnd().slice(0, Math.min(limit, s.length));
|
|
77
|
-
}
|
|
78
|
-
const omittedCount = (kept) => s.length - kept;
|
|
79
|
-
const markerFor = (kept) => `\n…[${s.length} chars total, ${omittedCount(kept)} omitted from the middle]…\n`;
|
|
80
|
-
const budget = limit - markerFor(0).length;
|
|
81
|
-
if (budget < 2)
|
|
82
|
-
return s.slice(0, limit);
|
|
83
|
-
const half = Math.floor(budget / 2);
|
|
84
|
-
const headEnd = half > 0 && isHighSurrogate(s.charCodeAt(half - 1)) ? half - 1 : half;
|
|
85
|
-
const tailStart = half > 0 && isLowSurrogate(s.charCodeAt(s.length - half)) ? s.length - half + 1 : s.length - half;
|
|
86
|
-
const kept = headEnd + (s.length - tailStart);
|
|
87
|
-
return `${s.slice(0, headEnd)}${markerFor(kept)}${s.slice(tailStart)}`;
|
|
69
|
+
return clipWithFilePointer(s, taskMaxOutputChars(), fullOutputPath);
|
|
88
70
|
}
|
|
89
71
|
function firstString(...values) {
|
|
90
72
|
for (const value of values) {
|
|
@@ -1092,6 +1074,8 @@ export class TaskRegistry {
|
|
|
1092
1074
|
handle.result = outcome.result;
|
|
1093
1075
|
handle.resultIsPartial = true;
|
|
1094
1076
|
backfilled = true;
|
|
1077
|
+
if (outcome.resultFull !== undefined)
|
|
1078
|
+
handle.resultFull = outcome.resultFull;
|
|
1095
1079
|
}
|
|
1096
1080
|
if (handle.error === undefined && outcome.error !== undefined && outcome.error !== BG_AGENT_REAP_STOP_ERROR) {
|
|
1097
1081
|
handle.error = outcome.error;
|
|
@@ -1101,6 +1085,7 @@ export class TaskRegistry {
|
|
|
1101
1085
|
handle.updatedAt = Date.now();
|
|
1102
1086
|
this.durableAgentWrite(handle, {
|
|
1103
1087
|
...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
|
|
1088
|
+
...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
|
|
1104
1089
|
...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
|
|
1105
1090
|
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1106
1091
|
});
|
|
@@ -1121,6 +1106,8 @@ export class TaskRegistry {
|
|
|
1121
1106
|
}
|
|
1122
1107
|
if (outcome.result !== undefined)
|
|
1123
1108
|
handle.result = outcome.result;
|
|
1109
|
+
if (outcome.resultFull !== undefined)
|
|
1110
|
+
handle.resultFull = outcome.resultFull;
|
|
1124
1111
|
if (outcome.status === "killed" && outcome.result !== undefined && outcome.result !== "") {
|
|
1125
1112
|
handle.resultIsPartial = true;
|
|
1126
1113
|
}
|
|
@@ -1137,6 +1124,7 @@ export class TaskRegistry {
|
|
|
1137
1124
|
settledAt: Date.now(),
|
|
1138
1125
|
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1139
1126
|
...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
|
|
1127
|
+
...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
|
|
1140
1128
|
...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
|
|
1141
1129
|
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1142
1130
|
}, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
@@ -1859,7 +1847,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1859
1847
|
const body = `status: ${row.status}
|
|
1860
1848
|
${row.error ? `error: ${row.error}
|
|
1861
1849
|
` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
1862
|
-
${clipTaskOutput(row.finalOutput)}` : "(no result text)"}`;
|
|
1850
|
+
${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
|
|
1863
1851
|
return {
|
|
1864
1852
|
content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
|
|
1865
1853
|
details: {
|
|
@@ -2037,13 +2025,14 @@ ${clipTaskOutput(row.finalOutput)}` : "(no result text)"}`;
|
|
|
2037
2025
|
if (abort !== undefined)
|
|
2038
2026
|
handle.abort = abort;
|
|
2039
2027
|
handle.result = undefined;
|
|
2028
|
+
handle.resultFull = undefined;
|
|
2040
2029
|
handle.error = undefined;
|
|
2041
2030
|
handle.resultIsPartial = undefined;
|
|
2042
2031
|
handle.stopSource = undefined;
|
|
2043
2032
|
handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
|
|
2044
2033
|
handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
|
|
2045
2034
|
handle.updatedAt = Date.now();
|
|
2046
|
-
this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "error", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]);
|
|
2035
|
+
this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]);
|
|
2047
2036
|
return { ok: true, cycle: handle.reviveCycle };
|
|
2048
2037
|
}
|
|
2049
2038
|
settleRevivedAgent(id, cycle, outcome) {
|
|
@@ -2597,7 +2586,7 @@ The agent is still working — you will be notified when it completes.`
|
|
|
2597
2586
|
: `status: ${handle.status}
|
|
2598
2587
|
${handle.error ? `error: ${handle.error}
|
|
2599
2588
|
` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
2600
|
-
${clipTaskOutput(handle.result, handle.outputFile)}` : "(no result text)"}`;
|
|
2589
|
+
${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no result text)"}`;
|
|
2601
2590
|
return {
|
|
2602
2591
|
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
2603
2592
|
details: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
export declare function truncateError(s: string): string;
|
|
3
|
+
export declare function clipWithFilePointer(s: string, limit: number, fullOutputPath?: string): string;
|
|
3
4
|
export declare function formatToolError(error: unknown): string;
|
|
4
5
|
export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "review" | "unexpected" | "oracle" | "resume" | "config" | "conflict" | "brain" | "aborted" | "unknown";
|
|
5
6
|
export declare function errorClassOf(errorCode: string | undefined): WorkerErrorClass;
|
package/dist/core/tool-errors.js
CHANGED
|
@@ -7,6 +7,27 @@ export function truncateError(s) {
|
|
|
7
7
|
const removed = s.length - MAX_ERROR_CHARS;
|
|
8
8
|
return `${s.slice(0, HALF)}\n\n... [${removed} characters truncated] ...\n\n${s.slice(s.length - HALF)}`;
|
|
9
9
|
}
|
|
10
|
+
const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
|
|
11
|
+
const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
|
|
12
|
+
export function clipWithFilePointer(s, limit, fullOutputPath) {
|
|
13
|
+
if (s.length <= limit)
|
|
14
|
+
return s;
|
|
15
|
+
if (fullOutputPath !== undefined) {
|
|
16
|
+
const header = `[Truncated. Full output: ${fullOutputPath}]\n\n`;
|
|
17
|
+
const room = limit - header.length;
|
|
18
|
+
return room > 0 ? header + s.slice(-room) : header.trimEnd().slice(0, Math.min(limit, s.length));
|
|
19
|
+
}
|
|
20
|
+
const omittedCount = (kept) => s.length - kept;
|
|
21
|
+
const markerFor = (kept) => `\n…[${s.length} chars total, ${omittedCount(kept)} omitted from the middle]…\n`;
|
|
22
|
+
const budget = limit - markerFor(0).length;
|
|
23
|
+
if (budget < 2)
|
|
24
|
+
return s.slice(0, limit);
|
|
25
|
+
const half = Math.floor(budget / 2);
|
|
26
|
+
const headEnd = half > 0 && isHighSurrogate(s.charCodeAt(half - 1)) ? half - 1 : half;
|
|
27
|
+
const tailStart = half > 0 && isLowSurrogate(s.charCodeAt(s.length - half)) ? s.length - half + 1 : s.length - half;
|
|
28
|
+
const kept = headEnd + (s.length - tailStart);
|
|
29
|
+
return `${s.slice(0, headEnd)}${markerFor(kept)}${s.slice(tailStart)}`;
|
|
30
|
+
}
|
|
10
31
|
function isShellLike(e) {
|
|
11
32
|
return typeof e === "object" && e !== null && ("exitCode" in e || "stderr" in e || "stdout" in e);
|
|
12
33
|
}
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -4,9 +4,10 @@ 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 { clipWithFilePointer } from "../../core/tool-errors.js";
|
|
7
8
|
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, similarNameSuggestion, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, OVERSIZE_READ_ESCAPE_HINT, isBlockedDevicePath, normalizeAbsPathLexically, } from "./safety.js";
|
|
8
9
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText } from "./encoding.js";
|
|
9
|
-
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern } from "./search.js";
|
|
10
|
+
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, shellQuote } from "./search.js";
|
|
10
11
|
import { makeRepoMapTool } from "./repo-map.js";
|
|
11
12
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
13
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE, sharpImageDownsampler } from "../../core/mcp.js";
|
|
@@ -61,12 +62,23 @@ const FILE_PATH_PARAMS = {
|
|
|
61
62
|
path: Type.Optional(Type.String({ description: "Deprecated alias for `file_path` (back-compat; prefer file_path)." })),
|
|
62
63
|
};
|
|
63
64
|
function clipShellOutput(s) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
return clipWithFilePointer(s, bashMaxOutputChars());
|
|
66
|
+
}
|
|
67
|
+
async function writeShellOverflowFile(env, stdout, stderr) {
|
|
68
|
+
const tf = await env.createTempFile({ prefix: "bash-output-", suffix: ".log" });
|
|
69
|
+
if (!tf.ok)
|
|
70
|
+
return undefined;
|
|
71
|
+
const body = stderr.length > 0 ? `${stdout}${stdout.length > 0 && !stdout.endsWith("\n") ? "\n" : ""}--- stderr ---\n${stderr}` : stdout;
|
|
72
|
+
const w = await env.writeFile(tf.value, body);
|
|
73
|
+
if (!w.ok)
|
|
74
|
+
return undefined;
|
|
75
|
+
const canon = await env.canonicalPath(tf.value);
|
|
76
|
+
return canon.ok ? canon.value : tf.value;
|
|
77
|
+
}
|
|
78
|
+
function shellRecoveryHint(path, readOnly) {
|
|
79
|
+
const quoted = shellQuote(path);
|
|
80
|
+
const example = readOnly ? `tail -c 50000 ${quoted}` : `sed -n 'START,ENDp' ${quoted}`;
|
|
81
|
+
return `\n(captured output preserved at ${path} — inspect with bash, e.g. \`${example}\`)`;
|
|
70
82
|
}
|
|
71
83
|
const FILE_STATE_TRAILER = " (file state is current in your context — no need to Read it back)";
|
|
72
84
|
const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
@@ -1446,7 +1458,7 @@ export function canAutoBackground(command) {
|
|
|
1446
1458
|
return false;
|
|
1447
1459
|
return true;
|
|
1448
1460
|
}
|
|
1449
|
-
async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef, detach, execClamp, toolCallId) {
|
|
1461
|
+
async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef, detach, execClamp, toolCallId, readOnly) {
|
|
1450
1462
|
let timeout = Math.min(BASH_MAX_TIMEOUT_SEC, Math.max(1, Math.floor(timeoutSec ?? BASH_DEFAULT_TIMEOUT_SEC)));
|
|
1451
1463
|
const requestedSec = timeout;
|
|
1452
1464
|
let clampedByDeadline = false;
|
|
@@ -1509,9 +1521,13 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1509
1521
|
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})`;
|
|
1510
1522
|
}
|
|
1511
1523
|
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1524
|
+
const rawStdout = res.error.partialStdout ?? "";
|
|
1525
|
+
const rawStderr = res.error.partialStderr ?? "";
|
|
1526
|
+
const cutOverflowFile = rawStdout.length > bashMaxOutputChars() || rawStderr.length > bashMaxOutputChars()
|
|
1527
|
+
? await writeShellOverflowFile(env, rawStdout, rawStderr)
|
|
1528
|
+
: undefined;
|
|
1529
|
+
const pStdout = clipShellOutput(rawStdout);
|
|
1530
|
+
const pStderr = clipShellOutput(rawStderr);
|
|
1515
1531
|
const headline = res.error.code === "timeout"
|
|
1516
1532
|
? clampedByDeadline
|
|
1517
1533
|
? deadlineExhausted
|
|
@@ -1526,13 +1542,15 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1526
1542
|
const body = captured.length > 0
|
|
1527
1543
|
? `\n${delimitUntrusted("partial command output", captured)}`
|
|
1528
1544
|
: `\n(no output was produced before the cutoff)`;
|
|
1545
|
+
const overflowNote = cutOverflowFile !== undefined ? shellRecoveryHint(cutOverflowFile, readOnly) : "";
|
|
1529
1546
|
return {
|
|
1530
|
-
content: `Error (${toolName}): ${headline}${body}`,
|
|
1547
|
+
content: `Error (${toolName}): ${headline}${body}${overflowNote}`,
|
|
1531
1548
|
details: {
|
|
1532
1549
|
type: "bash",
|
|
1533
1550
|
stdout: pStdout,
|
|
1534
1551
|
stderr: pStderr,
|
|
1535
1552
|
exitCode: null,
|
|
1553
|
+
...(cutOverflowFile !== undefined ? { output_file: cutOverflowFile } : {}),
|
|
1536
1554
|
...(res.error.code === "timeout"
|
|
1537
1555
|
? {
|
|
1538
1556
|
timedOut: true,
|
|
@@ -1567,26 +1585,38 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
1567
1585
|
cwdRef.current = captured;
|
|
1568
1586
|
}
|
|
1569
1587
|
}
|
|
1570
|
-
const
|
|
1571
|
-
const
|
|
1572
|
-
const clippedStderr = clip(stderr);
|
|
1588
|
+
const clippedStdout = clipShellOutput(stdout);
|
|
1589
|
+
const clippedStderr = clipShellOutput(stderr);
|
|
1573
1590
|
const stdoutImage = dataUriImageFromStdout(stdout);
|
|
1574
1591
|
if (stdoutImage) {
|
|
1592
|
+
const imgOverflowFile = stderr.length > bashMaxOutputChars() ? await writeShellOverflowFile(env, "", stderr) : undefined;
|
|
1593
|
+
const imgOverflowNote = imgOverflowFile !== undefined ? shellRecoveryHint(imgOverflowFile, readOnly) : "";
|
|
1575
1594
|
return {
|
|
1576
1595
|
content: [
|
|
1577
1596
|
{ type: "image", data: stdoutImage.data, mimeType: stdoutImage.mime },
|
|
1578
|
-
{ type: "text", text: `exit code: ${exitCode}\n[Image data detected in stdout and shown above]${stderr.trim() !== "" ? `\n--- stderr ---\n${clippedStderr}` : ""}` },
|
|
1597
|
+
{ type: "text", text: `exit code: ${exitCode}\n[Image data detected in stdout and shown above]${stderr.trim() !== "" ? `\n--- stderr ---\n${clippedStderr}` : ""}${imgOverflowNote}` },
|
|
1579
1598
|
],
|
|
1580
|
-
details: { type: "bash", stdout: "[image data]", stderr: clippedStderr, exitCode, isImage: true },
|
|
1599
|
+
details: { type: "bash", stdout: "[image data]", stderr: clippedStderr, exitCode, isImage: true, ...(imgOverflowFile !== undefined ? { output_file: imgOverflowFile } : {}) },
|
|
1581
1600
|
};
|
|
1582
1601
|
}
|
|
1602
|
+
const cleanOverflowFile = stdout.length > bashMaxOutputChars() || stderr.length > bashMaxOutputChars() ? await writeShellOverflowFile(env, stdout, stderr) : undefined;
|
|
1603
|
+
const overflowNote = cleanOverflowFile !== undefined ? shellRecoveryHint(cleanOverflowFile, readOnly) : "";
|
|
1583
1604
|
const exit1Note = exitCode === 1 ? bashExitOneInterpretation(command) : undefined;
|
|
1584
1605
|
const parts = [`exit code: ${exitCode}${exit1Note ? ` (${exit1Note} — exit 1 from this command is not an error)` : ""}`, `--- stdout ---\n${clippedStdout || "(empty)"}`];
|
|
1585
1606
|
if (stderr.trim() !== "")
|
|
1586
1607
|
parts.push(`--- stderr ---\n${clippedStderr}`);
|
|
1608
|
+
if (overflowNote)
|
|
1609
|
+
parts.push(overflowNote.trim());
|
|
1587
1610
|
return {
|
|
1588
1611
|
content: parts.join("\n"),
|
|
1589
|
-
details: {
|
|
1612
|
+
details: {
|
|
1613
|
+
type: "bash",
|
|
1614
|
+
stdout: clippedStdout,
|
|
1615
|
+
stderr: clippedStderr,
|
|
1616
|
+
exitCode,
|
|
1617
|
+
...(exit1Note ? { returnCodeInterpretation: exit1Note } : {}),
|
|
1618
|
+
...(cleanOverflowFile !== undefined ? { output_file: cleanOverflowFile } : {}),
|
|
1619
|
+
},
|
|
1590
1620
|
};
|
|
1591
1621
|
}
|
|
1592
1622
|
function bashDescription(coAuthor, bgNotifies = false, bgRetained = false) {
|
|
@@ -1828,7 +1858,7 @@ export function makeBashTool(env, rootCanonical, coAuthor = false, cwdRef = { cu
|
|
|
1828
1858
|
: undefined;
|
|
1829
1859
|
let res;
|
|
1830
1860
|
try {
|
|
1831
|
-
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, cwdRef, detachChain, taskOpts.execClamp, ctx.toolCallId);
|
|
1861
|
+
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, cwdRef, detachChain, taskOpts.execClamp, ctx.toolCallId, false);
|
|
1832
1862
|
}
|
|
1833
1863
|
finally {
|
|
1834
1864
|
taskOpts.detachHub?.gc(ctx.toolCallId);
|
|
@@ -1863,7 +1893,7 @@ export function makeBashReadonlyTool(env, rootCanonical, allow, execClamp) {
|
|
|
1863
1893
|
const reason = coarseReadonlyCheck(command, allow);
|
|
1864
1894
|
if (reason)
|
|
1865
1895
|
return `Error (Bash): ${reason}`;
|
|
1866
|
-
return runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, undefined, undefined, execClamp, ctx.toolCallId);
|
|
1896
|
+
return runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
1867
1897
|
},
|
|
1868
1898
|
});
|
|
1869
1899
|
}
|