@sema-agent/core 1.451.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-transcript-tool.d.ts +15 -0
- package/dist/agents/agent-transcript-tool.js +122 -0
- package/dist/agents/cascade.js +5 -8
- package/dist/agents/observer.js +3 -0
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/repair-loop.js +1 -1
- package/dist/agents/retain-ledger.d.ts +60 -0
- package/dist/agents/retain-ledger.js +225 -0
- package/dist/agents/roster-store.d.ts +1 -0
- package/dist/agents/roster-store.js +9 -0
- package/dist/agents/send-message-tool.d.ts +41 -0
- package/dist/agents/send-message-tool.js +469 -0
- package/dist/agents/subagent.d.ts +33 -108
- package/dist/agents/subagent.js +40 -813
- package/dist/agents/teacher.d.ts +0 -1
- package/dist/agents/teacher.js +0 -9
- package/dist/bin/sema-tb.js +3 -3
- package/dist/brain/anthropic.js +15 -40
- package/dist/brain/openai.js +15 -40
- package/dist/brain/reasoning.d.ts +1 -1
- package/dist/brain/stream-engine.js +2 -2
- package/dist/brain/stream-shared.d.ts +17 -0
- package/dist/brain/stream-shared.js +50 -0
- package/dist/brain/timeout.d.ts +1 -1
- package/dist/brain/timeout.js +1 -1
- package/dist/config/catalog.js +1 -3
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +6 -0
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-store.d.ts +0 -1
- package/dist/core/exec-gate.d.ts +1 -1
- package/dist/core/exec-output-tail.d.ts +19 -1
- package/dist/core/exec-output-tail.js +44 -5
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/git-worktree-env.d.ts +1 -1
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/image-downsample.d.ts +44 -0
- package/dist/core/image-downsample.js +97 -0
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +11 -1
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/mcp.d.ts +3 -45
- package/dist/core/mcp.js +2 -97
- package/dist/core/media-byte-cap.d.ts +1 -1
- package/dist/core/memory-recall.d.ts +0 -12
- package/dist/core/memory-recall.js +0 -222
- package/dist/core/memory.d.ts +0 -6
- package/dist/core/memory.js +0 -88
- package/dist/core/message-utils.d.ts +1 -1
- package/dist/core/oracle-isolation.d.ts +1 -1
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/roles.d.ts +1 -1
- package/dist/core/runner/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/assemble-result.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-memory.d.ts +16 -0
- package/dist/core/runner/prepare-memory.js +191 -0
- package/dist/core/runner/prepare-task.d.ts +4 -28
- package/dist/core/runner/prepare-task.js +29 -208
- package/dist/core/runner/prompt-suggestions.d.ts +1 -1
- package/dist/core/runner/runtask.d.ts +2 -3
- package/dist/core/runner/runtask.js +337 -529
- package/dist/core/runner/session-rule-policy.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.d.ts +5 -5
- package/dist/core/runner/synthetic-tools.js +4 -4
- package/dist/core/runner/tool-disclosure.d.ts +3 -3
- package/dist/core/runner/tool-disclosure.js +2 -2
- package/dist/core/runner/tool-output-projection.d.ts +6 -0
- package/dist/core/runner/tool-output-projection.js +81 -0
- package/dist/core/runtime.d.ts +1 -1
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/session-reconcile.d.ts +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/session-store.d.ts +2 -1
- package/dist/core/session-store.js +2 -1
- package/dist/core/session.d.ts +0 -1
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry-agent.d.ts +115 -0
- package/dist/core/task-registry-agent.js +1163 -0
- package/dist/core/task-registry-monitor.d.ts +13 -0
- package/dist/core/task-registry-monitor.js +347 -0
- package/dist/core/task-registry-shared.d.ts +294 -0
- package/dist/core/task-registry-shared.js +271 -0
- package/dist/core/task-registry-workflow.d.ts +5 -0
- package/dist/core/task-registry-workflow.js +143 -0
- package/dist/core/task-registry.d.ts +54 -213
- package/dist/core/task-registry.js +190 -1766
- package/dist/core/task-tool-shape.d.ts +32 -0
- package/dist/core/task-tool-shape.js +83 -0
- package/dist/core/tool-policy.d.ts +1 -2
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +2 -2
- package/dist/core/tool-result-store.js +3 -3
- package/dist/core/tools.d.ts +7 -3
- package/dist/core/tools.js +3 -8
- package/dist/core/trace.d.ts +2 -2
- package/dist/core/types.d.ts +1 -29
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/core/workflow-run-store-contract.d.ts +1 -1
- package/dist/core/workflow-run-store-contract.js +19 -19
- package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
- package/dist/engine/execution-env/node-execution-env.js +184 -44
- package/dist/engine/lsp/frame-decoder.d.ts +1 -1
- package/dist/engine/lsp/frame-decoder.js +1 -1
- package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
- package/dist/index.d.ts +18 -16
- package/dist/index.js +15 -13
- package/dist/internal/harness-types.d.ts +7 -0
- package/dist/internal/harness-types.js +1 -0
- package/dist/internal/harness.d.ts +1 -7
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/goal.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +2 -1
- package/dist/orchestration/run-workflow-tool.js +29 -6
- package/dist/orchestration/workflow-types.d.ts +193 -0
- package/dist/orchestration/workflow-types.js +54 -0
- package/dist/orchestration/workflow.d.ts +3 -184
- package/dist/orchestration/workflow.js +87 -64
- package/dist/prompt-assembly/assemble.d.ts +3 -3
- package/dist/prompt-assembly/assemble.js +0 -16
- package/dist/prompt-assembly/explain.d.ts +1 -1
- package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
- package/dist/prompts/default.d.ts +1 -6
- package/dist/prompts/default.js +3 -11
- package/dist/scenarios/env.d.ts +1 -1
- package/dist/scenarios/full-body.d.ts +0 -3
- package/dist/scenarios/full-body.js +0 -2
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.d.ts +2 -15
- package/dist/stores/file/background-agent-store.js +33 -102
- package/dist/stores/file/checkpoint-store.d.ts +1 -14
- package/dist/stores/file/checkpoint-store.js +47 -119
- package/dist/stores/file/file-snapshot-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/stores/file/shared-ledger.d.ts +35 -0
- package/dist/stores/file/shared-ledger.js +94 -0
- package/dist/stores/file/workflow-run-store.d.ts +1 -14
- package/dist/stores/file/workflow-run-store.js +24 -93
- package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
- package/dist/tools/fs/bash-readonly-classifier.js +166 -0
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/fs-bash.d.ts +31 -0
- package/dist/tools/fs/fs-bash.js +672 -0
- package/dist/tools/fs/fs-pdf.d.ts +22 -0
- package/dist/tools/fs/fs-pdf.js +236 -0
- package/dist/tools/fs/fs-read.d.ts +8 -0
- package/dist/tools/fs/fs-read.js +286 -0
- package/dist/tools/fs/fs-search-tools.d.ts +5 -0
- package/dist/tools/fs/fs-search-tools.js +173 -0
- package/dist/tools/fs/fs-shared.d.ts +55 -0
- package/dist/tools/fs/fs-shared.js +144 -0
- package/dist/tools/fs/fs-write.d.ts +8 -0
- package/dist/tools/fs/fs-write.js +423 -0
- package/dist/tools/fs/index.d.ts +12 -65
- package/dist/tools/fs/index.js +26 -2026
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/pdf.d.ts +1 -1
- package/dist/tools/fs/repo-map.d.ts +3 -3
- package/dist/tools/fs/repo-map.js +3 -3
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +1 -1
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.d.ts +1 -1
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +4 -2
- package/dist/tools/web.js +106 -20
- package/dist/tools/worktree.d.ts +1 -1
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
- package/dist/core/dynamic-recall.d.ts +0 -32
- package/dist/core/dynamic-recall.js +0 -76
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
+
import { defaultTaskRegistry } from "../../core/task-registry.js";
|
|
4
|
+
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
|
|
5
|
+
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
6
|
+
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
7
|
+
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
8
|
+
import { imageMagicMatches } from "./safety.js";
|
|
9
|
+
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
10
|
+
import { BASH_DEFAULT_TIMEOUT_SEC, BASH_MAX_TIMEOUT_SEC, BASH_DEFAULT_TIMEOUT_MS, BASH_MAX_TIMEOUT_MS, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, msTimeoutToSec, } from "./fs-shared.js";
|
|
11
|
+
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
|
|
12
|
+
export function bashReversibilityProbe(allow) {
|
|
13
|
+
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
14
|
+
return (args) => {
|
|
15
|
+
const command = args?.command;
|
|
16
|
+
if (typeof command !== "string")
|
|
17
|
+
return { reversible: false };
|
|
18
|
+
return { reversible: classifyCompoundReadonly(command, allowSet) === undefined };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const EXIT1_INTERPRETATION = {
|
|
22
|
+
grep: "No matches found",
|
|
23
|
+
rg: "No matches found",
|
|
24
|
+
egrep: "No matches found",
|
|
25
|
+
fgrep: "No matches found",
|
|
26
|
+
find: "Some directories were inaccessible",
|
|
27
|
+
diff: "Files differ",
|
|
28
|
+
test: "Condition is false",
|
|
29
|
+
"[": "Condition is false",
|
|
30
|
+
};
|
|
31
|
+
export function bashExitOneInterpretation(command) {
|
|
32
|
+
const stmts = command.split(/\n|;|&&|\|\|/);
|
|
33
|
+
let lastStmt = "";
|
|
34
|
+
for (let i = stmts.length - 1; i >= 0; i--) {
|
|
35
|
+
if (stmts[i].trim().length > 0) {
|
|
36
|
+
lastStmt = stmts[i];
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const lastSeg = lastStmt.split("|").at(-1)?.trim() ?? "";
|
|
41
|
+
const tokens = lastSeg.split(/\s+/).filter(Boolean);
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]))
|
|
44
|
+
i++;
|
|
45
|
+
const name = tokens[i]?.split("/").pop() ?? "";
|
|
46
|
+
if (name === "git") {
|
|
47
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
48
|
+
const t = tokens[j];
|
|
49
|
+
if (t.startsWith("-")) {
|
|
50
|
+
if (t === "-C" || t === "-c")
|
|
51
|
+
j++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (t === "grep")
|
|
55
|
+
return "No matches found";
|
|
56
|
+
if (t === "diff")
|
|
57
|
+
return "Files differ";
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return EXIT1_INTERPRETATION[name];
|
|
63
|
+
}
|
|
64
|
+
function dataUriImageFromStdout(stdout) {
|
|
65
|
+
const t = stdout.trim();
|
|
66
|
+
const m = /^data:image\/[a-z0-9.+_-]+;base64,([A-Za-z0-9+/=\s]+)$/i.exec(t);
|
|
67
|
+
if (!m)
|
|
68
|
+
return undefined;
|
|
69
|
+
const data = m[1].replace(/\s/g, "");
|
|
70
|
+
if (data.length === 0 || data.length > MCP_IMAGE_MAX_BASE64)
|
|
71
|
+
return undefined;
|
|
72
|
+
const bytes = Buffer.from(data, "base64");
|
|
73
|
+
for (const mime of ["image/png", "image/jpeg", "image/gif", "image/webp"]) {
|
|
74
|
+
if (imageMagicMatches(bytes, mime))
|
|
75
|
+
return { data, mime };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
async function isDeadCwd(env, dir) {
|
|
80
|
+
const info = await env.fileInfo(dir);
|
|
81
|
+
if (!info.ok)
|
|
82
|
+
return info.error.code === "not_found";
|
|
83
|
+
if (info.value.kind === "directory")
|
|
84
|
+
return false;
|
|
85
|
+
if (info.value.kind === "symlink") {
|
|
86
|
+
const target = await env.canonicalPath(dir);
|
|
87
|
+
return !target.ok;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef) {
|
|
92
|
+
if (!(await isDeadCwd(env, effectiveCwd)))
|
|
93
|
+
return undefined;
|
|
94
|
+
if (cwdRef && cwdRef.current !== root) {
|
|
95
|
+
if (!(await isDeadCwd(env, root))) {
|
|
96
|
+
cwdRef.current = root;
|
|
97
|
+
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
|
|
98
|
+
`(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
|
|
99
|
+
`re-run your command (cd first if you need a different directory).`);
|
|
100
|
+
}
|
|
101
|
+
return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
|
|
102
|
+
`The shell has no usable directory to run in; this task cannot recover its working tree.`);
|
|
103
|
+
}
|
|
104
|
+
return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
|
|
105
|
+
`It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
|
|
106
|
+
`this task cannot recover its working tree.`);
|
|
107
|
+
}
|
|
108
|
+
const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
|
|
109
|
+
function stripAutoBackgroundWrapperPrefixes(segment) {
|
|
110
|
+
let s = segment.trim();
|
|
111
|
+
const wrappers = [
|
|
112
|
+
/^timeout\s+(?:-[A-Za-z]\S*\s+)*\d+(?:\.\d+)?[smhd]?\s+/,
|
|
113
|
+
/^time\s+/,
|
|
114
|
+
/^nice(?:\s+-n?\s*-?\d+)?\s+/,
|
|
115
|
+
/^nohup\s+/,
|
|
116
|
+
/^command\s+/,
|
|
117
|
+
/^builtin\s+/,
|
|
118
|
+
/^stdbuf(?:\s+-[ioe]\S+)+\s+/,
|
|
119
|
+
/^noglob\s+/,
|
|
120
|
+
/^[A-Za-z_][A-Za-z0-9_]*=\S+\s+/,
|
|
121
|
+
];
|
|
122
|
+
let changed = true;
|
|
123
|
+
while (changed) {
|
|
124
|
+
changed = false;
|
|
125
|
+
for (const re of wrappers) {
|
|
126
|
+
const stripped = s.replace(re, "");
|
|
127
|
+
if (stripped !== s) {
|
|
128
|
+
s = stripped.trim();
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return s;
|
|
134
|
+
}
|
|
135
|
+
function commandBasename(token) {
|
|
136
|
+
const idx = token.lastIndexOf("/");
|
|
137
|
+
return idx === -1 ? token : token.slice(idx + 1);
|
|
138
|
+
}
|
|
139
|
+
export function canAutoBackground(command) {
|
|
140
|
+
const segments = command
|
|
141
|
+
.split(/&&|\|\||;|\||\r?\n/)
|
|
142
|
+
.map((s) => s.trim())
|
|
143
|
+
.filter((s) => s.length > 0);
|
|
144
|
+
if (segments.length === 0)
|
|
145
|
+
return true;
|
|
146
|
+
for (const seg of segments) {
|
|
147
|
+
const tokens = stripAutoBackgroundWrapperPrefixes(seg).split(/\s+/);
|
|
148
|
+
const word = commandBasename(tokens[0] ?? "");
|
|
149
|
+
if (word === "git")
|
|
150
|
+
return false;
|
|
151
|
+
if (word === "xargs" && tokens.slice(1).some((t) => commandBasename(t) === "git"))
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const firstWord = commandBasename(stripAutoBackgroundWrapperPrefixes(segments[0]).split(/\s+/)[0] ?? "");
|
|
155
|
+
if (AUTO_BACKGROUND_BLOCKED_FIRST_WORDS.has(firstWord))
|
|
156
|
+
return false;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef, detach, execClamp, toolCallId, readOnly) {
|
|
160
|
+
let timeout = Math.min(BASH_MAX_TIMEOUT_SEC, Math.max(1, Math.floor(timeoutSec ?? BASH_DEFAULT_TIMEOUT_SEC)));
|
|
161
|
+
const requestedSec = timeout;
|
|
162
|
+
let clampedByDeadline = false;
|
|
163
|
+
let deadlineExhausted = false;
|
|
164
|
+
if (execClamp) {
|
|
165
|
+
const deadline = execClamp.deadlineMs();
|
|
166
|
+
if (deadline !== undefined) {
|
|
167
|
+
const rawRemainSec = Math.floor((deadline - Date.now()) / 1000);
|
|
168
|
+
const maxSec = Math.max(1, rawRemainSec);
|
|
169
|
+
if (maxSec < timeout) {
|
|
170
|
+
execClamp.onClamp?.(timeout, maxSec);
|
|
171
|
+
timeout = maxSec;
|
|
172
|
+
clampedByDeadline = true;
|
|
173
|
+
deadlineExhausted = rawRemainSec < 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const effectiveCwd = cwdRef ? cwdRef.current : cwd;
|
|
178
|
+
const toRun = cwdRef
|
|
179
|
+
? `${command}\n__cc_rc=$?; printf '%s%s%s' '${CWD_SENTINEL}' "$(pwd)" '${CWD_SENTINEL}' 1>&2; exit $__cc_rc`
|
|
180
|
+
: command;
|
|
181
|
+
const execEnv = cwdRef ? { env: { PWD: effectiveCwd } } : undefined;
|
|
182
|
+
let deregisterKill;
|
|
183
|
+
const registerKillHandle = execClamp?.registerKillHandle;
|
|
184
|
+
let res;
|
|
185
|
+
try {
|
|
186
|
+
res = await env.exec(toRun, {
|
|
187
|
+
cwd: effectiveCwd,
|
|
188
|
+
timeout,
|
|
189
|
+
abortSignal: signal,
|
|
190
|
+
...execEnv,
|
|
191
|
+
...(detach
|
|
192
|
+
? {
|
|
193
|
+
detachSignal: detach.signal,
|
|
194
|
+
onDetachAdopted: () => {
|
|
195
|
+
deregisterKill?.();
|
|
196
|
+
deregisterKill = undefined;
|
|
197
|
+
},
|
|
198
|
+
autoBackgroundOnTimeout: detach.autoBackgroundEligible === true && !clampedByDeadline,
|
|
199
|
+
}
|
|
200
|
+
: {}),
|
|
201
|
+
...(registerKillHandle !== undefined && toolCallId !== undefined
|
|
202
|
+
? {
|
|
203
|
+
onSpawn: (handle) => {
|
|
204
|
+
deregisterKill = registerKillHandle(toolCallId, handle);
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
deregisterKill?.();
|
|
212
|
+
}
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
|
|
215
|
+
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
|
|
216
|
+
throw new Error(diagnosis ?? `Error (${toolName}): command did not run (${res.error.code}): ${res.error.message}`);
|
|
217
|
+
}
|
|
218
|
+
if (res.error.code === "transport_lost") {
|
|
219
|
+
return {
|
|
220
|
+
content: `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})`,
|
|
221
|
+
details: { type: "bash", transportLost: true },
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
226
|
+
const rawStdout = res.error.partialStdout ?? "";
|
|
227
|
+
const rawStderr = res.error.partialStderr ?? "";
|
|
228
|
+
const cutOverflowFile = rawStdout.length > bashMaxOutputChars() || rawStderr.length > bashMaxOutputChars()
|
|
229
|
+
? await writeShellOverflowFile(env, rawStdout, rawStderr)
|
|
230
|
+
: undefined;
|
|
231
|
+
const pStdout = clipShellOutput(rawStdout);
|
|
232
|
+
const pStderr = clipShellOutput(rawStderr);
|
|
233
|
+
const headline = res.error.code === "timeout"
|
|
234
|
+
? clampedByDeadline
|
|
235
|
+
? deadlineExhausted
|
|
236
|
+
? `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.`
|
|
237
|
+
: `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.`
|
|
238
|
+
: `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.`
|
|
239
|
+
: res.error.code === "aborted"
|
|
240
|
+
? `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`
|
|
241
|
+
: `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.`;
|
|
242
|
+
const captured = (pStdout ? `--- partial stdout ---\n${pStdout}` : "") +
|
|
243
|
+
(pStderr ? `${pStdout ? "\n" : ""}--- partial stderr ---\n${pStderr}` : "");
|
|
244
|
+
const body = captured.length > 0
|
|
245
|
+
? `\n${delimitUntrusted("partial command output", captured)}`
|
|
246
|
+
: `\n(no output was produced before the cutoff)`;
|
|
247
|
+
const overflowNote = cutOverflowFile !== undefined ? shellRecoveryHint(cutOverflowFile, readOnly) : "";
|
|
248
|
+
return {
|
|
249
|
+
content: `Error (${toolName}): ${headline}${body}${overflowNote}`,
|
|
250
|
+
details: {
|
|
251
|
+
type: "bash",
|
|
252
|
+
stdout: pStdout,
|
|
253
|
+
stderr: pStderr,
|
|
254
|
+
exitCode: null,
|
|
255
|
+
...(cutOverflowFile !== undefined ? { output_file: cutOverflowFile } : {}),
|
|
256
|
+
...(res.error.code === "timeout"
|
|
257
|
+
? {
|
|
258
|
+
timedOut: true,
|
|
259
|
+
timeoutSec: timeout,
|
|
260
|
+
...(clampedByDeadline ? { requestedTimeoutSec: requestedSec, clampedByDeadline: true } : {}),
|
|
261
|
+
}
|
|
262
|
+
: res.error.code === "aborted"
|
|
263
|
+
? { aborted: true }
|
|
264
|
+
: { callbackError: true }),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
content: `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.`,
|
|
270
|
+
details: { type: "bash" },
|
|
271
|
+
isError: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (res.value.detached && detach) {
|
|
275
|
+
return await detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr, res.value.detached.cause);
|
|
276
|
+
}
|
|
277
|
+
let { stderr } = res.value;
|
|
278
|
+
const { stdout, exitCode } = res.value;
|
|
279
|
+
if (cwdRef) {
|
|
280
|
+
const m = stderr.match(new RegExp(`${CWD_SENTINEL}([\\s\\S]*?)${CWD_SENTINEL}`));
|
|
281
|
+
if (m) {
|
|
282
|
+
stderr = stderr.replace(m[0], "");
|
|
283
|
+
const captured = m[1].trim();
|
|
284
|
+
const windowsWorld = /^[A-Za-z]:[\\/]/.test(cwdRef.current);
|
|
285
|
+
const msys = /^\/([A-Za-z])(\/.*)?$/.exec(captured);
|
|
286
|
+
if (/^[A-Za-z]:[\\/]/.test(captured))
|
|
287
|
+
cwdRef.current = captured;
|
|
288
|
+
else if (windowsWorld && msys)
|
|
289
|
+
cwdRef.current = `${msys[1]}:${msys[2] ?? "/"}`;
|
|
290
|
+
else if (captured.startsWith("/") && !windowsWorld)
|
|
291
|
+
cwdRef.current = captured;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const clippedStdout = clipShellOutput(stdout);
|
|
295
|
+
const clippedStderr = clipShellOutput(stderr);
|
|
296
|
+
const stdoutImage = dataUriImageFromStdout(stdout);
|
|
297
|
+
if (stdoutImage) {
|
|
298
|
+
const imgOverflowFile = stderr.length > bashMaxOutputChars() ? await writeShellOverflowFile(env, "", stderr) : undefined;
|
|
299
|
+
const imgOverflowNote = imgOverflowFile !== undefined ? shellRecoveryHint(imgOverflowFile, readOnly) : "";
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{ type: "image", data: stdoutImage.data, mimeType: stdoutImage.mime },
|
|
303
|
+
{ type: "text", text: `exit code: ${exitCode}\n[Image data detected in stdout and shown above]${stderr.trim() !== "" ? `\n--- stderr ---\n${clippedStderr}` : ""}${imgOverflowNote}` },
|
|
304
|
+
],
|
|
305
|
+
details: { type: "bash", stdout: "[image data]", stderr: clippedStderr, exitCode, isImage: true, ...(imgOverflowFile !== undefined ? { output_file: imgOverflowFile } : {}) },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const cleanOverflowFile = stdout.length > bashMaxOutputChars() || stderr.length > bashMaxOutputChars() ? await writeShellOverflowFile(env, stdout, stderr) : undefined;
|
|
309
|
+
const overflowNote = cleanOverflowFile !== undefined ? shellRecoveryHint(cleanOverflowFile, readOnly) : "";
|
|
310
|
+
const exit1Note = exitCode === 1 ? bashExitOneInterpretation(command) : undefined;
|
|
311
|
+
const parts = [`exit code: ${exitCode}${exit1Note ? ` (${exit1Note} — exit 1 from this command is not an error)` : ""}`, `--- stdout ---\n${clippedStdout || "(empty)"}`];
|
|
312
|
+
if (stderr.trim() !== "")
|
|
313
|
+
parts.push(`--- stderr ---\n${clippedStderr}`);
|
|
314
|
+
if (overflowNote)
|
|
315
|
+
parts.push(overflowNote.trim());
|
|
316
|
+
return {
|
|
317
|
+
content: parts.join("\n"),
|
|
318
|
+
details: {
|
|
319
|
+
type: "bash",
|
|
320
|
+
stdout: clippedStdout,
|
|
321
|
+
stderr: clippedStderr,
|
|
322
|
+
exitCode,
|
|
323
|
+
...(exit1Note ? { returnCodeInterpretation: exit1Note } : {}),
|
|
324
|
+
...(cleanOverflowFile !== undefined ? { output_file: cleanOverflowFile } : {}),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function bashDescription(coAuthor, bgNotifies = false, bgRetained = false) {
|
|
329
|
+
const coAuthorLines = coAuthor === false
|
|
330
|
+
? "- Follow the deployment's commit-message conventions."
|
|
331
|
+
: `- End git commit messages with:\nCo-Authored-By: ${coAuthor}`;
|
|
332
|
+
return `Executes a bash command and returns its output.
|
|
333
|
+
|
|
334
|
+
- Working directory persists between calls, but prefer absolute paths — a \`cd\` carries over and moves the base for every later command. Shell state (env vars, functions) does not persist; the shell is re-initialized each call, starting at the configured root.
|
|
335
|
+
- IMPORTANT: Avoid using this tool to run \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user.
|
|
336
|
+
- File search by name: use the Glob tool (NOT \`find\` or \`ls\`); content search: use the Grep tool (NOT \`grep\`/\`rg\` in the shell).
|
|
337
|
+
- \`timeout\` is in milliseconds: default ${BASH_DEFAULT_TIMEOUT_MS}, max ${BASH_MAX_TIMEOUT_MS}.
|
|
338
|
+
- \`run_in_background\` runs the command detached: it keeps running across turns and ${bgNotifies ? "re-invokes you when it exits" : "you read its output later with TaskOutput(task_id)"}. No \`&\` needed.${bgRetained ? "" : " Background processes do NOT survive the session — they are reaped when the task ends. A deliverable that must stay alive afterwards (a server, a daemon) needs a self-detaching FOREGROUND start instead: run `nohup cmd >log 2>&1 &` as a normal foreground command (portable; setsid does not exist on macOS), or use a service manager."}
|
|
339
|
+
|
|
340
|
+
# Git
|
|
341
|
+
- Interactive flags (\`-i\`, e.g. \`git rebase -i\`, \`git add -i\`) are not supported in this environment.
|
|
342
|
+
- Use the \`gh\` CLI for GitHub operations (PRs, issues, API).
|
|
343
|
+
- Commit or push only when the user asks. If on the default branch, branch first.
|
|
344
|
+
${coAuthorLines}
|
|
345
|
+
|
|
346
|
+
# Notes
|
|
347
|
+
- The shell is NOT sandboxed by this tool: a command can read or write any reachable path and use the network. Every call is subject to the deployment's approval policy (it may be denied or require confirmation); a denied call should not be retried verbatim.
|
|
348
|
+
- Treat evidence of a deployment restriction — "Operation not permitted", "Read-only file system", a denied path, or a network failure — as a boundary, not a bug: adjust within the allowed scope or report the limit; don't reach for a destructive or privilege-escalating workaround.`;
|
|
349
|
+
}
|
|
350
|
+
export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
|
|
351
|
+
const bgNotifies = taskOpts.taskRegistry !== undefined && taskOpts.onTaskNotification !== undefined;
|
|
352
|
+
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
353
|
+
return defineTool({
|
|
354
|
+
name: "Bash",
|
|
355
|
+
contract: { contractId: "core.bash@1", implementationRevision: "1" },
|
|
356
|
+
description: bashDescription(coAuthor, bgNotifies, bgRetained),
|
|
357
|
+
parameters: Type.Object({
|
|
358
|
+
command: Type.String({ description: "The command to execute" }),
|
|
359
|
+
timeout: Type.Optional(Type.Number({ description: `Optional timeout in milliseconds (max ${BASH_MAX_TIMEOUT_MS})` })),
|
|
360
|
+
description: Type.Optional(Type.String({
|
|
361
|
+
description: 'Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.\n' +
|
|
362
|
+
"\n" +
|
|
363
|
+
"For simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):\n" +
|
|
364
|
+
'- ls → "List files in current directory"\n' +
|
|
365
|
+
'- git status → "Show working tree status"\n' +
|
|
366
|
+
'- npm install → "Install package dependencies"\n' +
|
|
367
|
+
"\n" +
|
|
368
|
+
"For commands that are harder to parse at a glance (piped commands, obscure flags, etc.), add enough context to clarify what it does:\n" +
|
|
369
|
+
'- find . -name "*.tmp" -exec rm {} \\; → "Find and delete all .tmp files recursively"\n' +
|
|
370
|
+
'- git reset --hard origin/main → "Discard all local changes and match remote main"\n' +
|
|
371
|
+
"- curl -s url | jq '.data[]' → \"Fetch JSON from URL and extract data array elements\"",
|
|
372
|
+
})),
|
|
373
|
+
run_in_background: Type.Optional(Type.Boolean({
|
|
374
|
+
description: "Set to true to run this command in the background.",
|
|
375
|
+
})),
|
|
376
|
+
}),
|
|
377
|
+
effect: "write",
|
|
378
|
+
isConcurrencySafe: (args) => {
|
|
379
|
+
const cmd = args?.command;
|
|
380
|
+
if (typeof cmd !== "string")
|
|
381
|
+
return false;
|
|
382
|
+
const bg = args.run_in_background;
|
|
383
|
+
if (bg === true)
|
|
384
|
+
return false;
|
|
385
|
+
return coarseReadonlyCheck(cmd, new Set(BASH_READONLY_DEFAULT_ALLOW)) === undefined;
|
|
386
|
+
},
|
|
387
|
+
execute: async (args, ctx) => {
|
|
388
|
+
const { command, timeout, run_in_background, description } = args;
|
|
389
|
+
if (run_in_background) {
|
|
390
|
+
if (!hasBackgroundShell(env)) {
|
|
391
|
+
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
392
|
+
}
|
|
393
|
+
if (cwdRef && (await isDeadCwd(env, cwdRef.current))) {
|
|
394
|
+
const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef);
|
|
395
|
+
if (diagnosis !== undefined)
|
|
396
|
+
throw new Error(diagnosis);
|
|
397
|
+
}
|
|
398
|
+
const requestedTimeoutSec = timeout !== undefined ? Math.max(1, Math.round(timeout / 1000)) : undefined;
|
|
399
|
+
const r = await env.spawnBackground(command, {
|
|
400
|
+
cwd: cwdRef.current,
|
|
401
|
+
...(requestedTimeoutSec !== undefined ? { timeout: requestedTimeoutSec } : {}),
|
|
402
|
+
});
|
|
403
|
+
if (!r.ok)
|
|
404
|
+
return errorResult(`Error (Bash): ${r.error.message}`);
|
|
405
|
+
const bgCaps = env.backgroundCapabilities;
|
|
406
|
+
const appliedTimeoutSec = typeof bgCaps.defaultBgTimeoutSec === "number" && typeof bgCaps.maxBgTimeoutSec === "number"
|
|
407
|
+
? Math.min(requestedTimeoutSec ?? bgCaps.defaultBgTimeoutSec, bgCaps.maxBgTimeoutSec)
|
|
408
|
+
: undefined;
|
|
409
|
+
const budgetNote = appliedTimeoutSec !== undefined ? ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).` : "";
|
|
410
|
+
const lifetimeNote = bgCaps.retainBackgroundProcesses === true
|
|
411
|
+
? ""
|
|
412
|
+
: ` NOTE: background processes do NOT survive the session (reaped at task end). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session).`;
|
|
413
|
+
if (taskOpts.taskRegistry) {
|
|
414
|
+
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
415
|
+
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
416
|
+
const scope = ctx.principal ?? taskOpts.taskScope;
|
|
417
|
+
const onNotify = taskOpts.onTaskNotification;
|
|
418
|
+
let outputFile;
|
|
419
|
+
if (onNotify !== undefined) {
|
|
420
|
+
const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
|
|
421
|
+
if (tf.ok) {
|
|
422
|
+
const tfCanon = await env.canonicalPath(tf.value);
|
|
423
|
+
outputFile = tfCanon.ok ? tfCanon.value : tf.value;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
let taskId;
|
|
427
|
+
try {
|
|
428
|
+
taskId = taskOpts.taskRegistry.registerBackgroundBash({
|
|
429
|
+
env,
|
|
430
|
+
shellId: r.value.shellId,
|
|
431
|
+
description: description ?? command,
|
|
432
|
+
toolUseId: ctx.toolCallId,
|
|
433
|
+
...(owner !== undefined ? { owner } : {}),
|
|
434
|
+
...(scope !== undefined ? { scope } : {}),
|
|
435
|
+
...(sessionScoped ? { sessionScoped: true } : {}),
|
|
436
|
+
...(outputFile !== undefined ? { outputFile } : {}),
|
|
437
|
+
...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
442
|
+
let kill;
|
|
443
|
+
try {
|
|
444
|
+
kill = await env.killBackground(r.value.shellId);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
kill = undefined;
|
|
448
|
+
}
|
|
449
|
+
if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
|
|
450
|
+
return errorResult(`Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`);
|
|
451
|
+
}
|
|
452
|
+
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
453
|
+
return {
|
|
454
|
+
content: `Error (Bash): background process was started but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
|
|
455
|
+
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
456
|
+
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
|
|
457
|
+
details: { type: "bash", registerFailed: true, killFailed: true },
|
|
458
|
+
isError: true,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const interimNote = outputFile !== undefined
|
|
462
|
+
? ` Output file: ${outputFile} (full output is appended there — Read it any time).`
|
|
463
|
+
: ` Use TaskOutput("${taskId}") to check interim output.`;
|
|
464
|
+
return onNotify !== undefined
|
|
465
|
+
? `Command running in background; task_id=${taskId}.${interimNote} You will be notified when it completes — do not poll; ` +
|
|
466
|
+
`end your turn if nothing else is pending. TaskStop("${taskId}") to stop.` +
|
|
467
|
+
budgetNote +
|
|
468
|
+
lifetimeNote
|
|
469
|
+
: `Started in background; task_id=${taskId}. ` +
|
|
470
|
+
`Use TaskOutput("${taskId}") to read output (poll until status is no longer "running"); ` +
|
|
471
|
+
`TaskStop("${taskId}") to stop. No '&' needed.` +
|
|
472
|
+
budgetNote +
|
|
473
|
+
lifetimeNote;
|
|
474
|
+
}
|
|
475
|
+
return (`Started in background; task_id=${r.value.shellId}. Legacy bash_id=${r.value.shellId}. ` +
|
|
476
|
+
`Use TaskOutput("${r.value.shellId}") to read output (poll until status is no longer "running"); ` +
|
|
477
|
+
`TaskStop("${r.value.shellId}") to stop. No '&' needed.` +
|
|
478
|
+
budgetNote +
|
|
479
|
+
lifetimeNote);
|
|
480
|
+
}
|
|
481
|
+
const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
|
|
482
|
+
? {
|
|
483
|
+
signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
|
|
484
|
+
onDetached: async (shellId, stdoutSoFar, stderrSoFar, cause) => {
|
|
485
|
+
const sessionScoped = taskOpts.sessionId !== undefined;
|
|
486
|
+
const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
|
|
487
|
+
const scope = ctx.principal ?? taskOpts.taskScope;
|
|
488
|
+
const onNotify = taskOpts.onTaskNotification;
|
|
489
|
+
let outputFile;
|
|
490
|
+
if (onNotify !== undefined) {
|
|
491
|
+
const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
|
|
492
|
+
if (tf.ok) {
|
|
493
|
+
const tfCanon = await env.canonicalPath(tf.value);
|
|
494
|
+
outputFile = tfCanon.ok ? tfCanon.value : tf.value;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
let taskId;
|
|
498
|
+
try {
|
|
499
|
+
taskId = taskOpts.taskRegistry.registerBackgroundBash({
|
|
500
|
+
env: env,
|
|
501
|
+
shellId: shellId,
|
|
502
|
+
description: description ?? command,
|
|
503
|
+
toolUseId: ctx.toolCallId,
|
|
504
|
+
...(owner !== undefined ? { owner } : {}),
|
|
505
|
+
...(scope !== undefined ? { scope } : {}),
|
|
506
|
+
...(sessionScoped ? { sessionScoped: true } : {}),
|
|
507
|
+
...(outputFile !== undefined ? { outputFile } : {}),
|
|
508
|
+
...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
513
|
+
let kill;
|
|
514
|
+
try {
|
|
515
|
+
kill = await env.killBackground(shellId);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
kill = undefined;
|
|
519
|
+
}
|
|
520
|
+
const killed = kill !== undefined && (kill.ok || kill.error.code === "not_found");
|
|
521
|
+
const outTail = stdoutSoFar.slice(-1_000);
|
|
522
|
+
const errTail = stderrSoFar.slice(-1_000);
|
|
523
|
+
const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
|
|
524
|
+
(errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
|
|
525
|
+
if (killed) {
|
|
526
|
+
return {
|
|
527
|
+
content: `Error (Bash): command was detached to the background but could not be registered (${reason}); ` +
|
|
528
|
+
`it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
|
|
529
|
+
tails,
|
|
530
|
+
details: { type: "bash", detached: true, registerFailed: true },
|
|
531
|
+
isError: true,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
|
|
535
|
+
return {
|
|
536
|
+
content: `Error (Bash): command was detached to the background but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
|
|
537
|
+
`The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
|
|
538
|
+
`If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
|
|
539
|
+
tails,
|
|
540
|
+
details: { type: "bash", detached: true, registerFailed: true, killFailed: true },
|
|
541
|
+
isError: true,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const tail = stdoutSoFar.slice(-1_000);
|
|
545
|
+
const adoptCaps = env.backgroundCapabilities;
|
|
546
|
+
const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
|
|
547
|
+
? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
|
|
548
|
+
: "";
|
|
549
|
+
return {
|
|
550
|
+
content: (cause === "timeout"
|
|
551
|
+
? `Command exceeded its timeout but is still running — moved to background instead of being killed (task_id=${taskId}). `
|
|
552
|
+
: `Command moved to background; task_id=${taskId}. `) +
|
|
553
|
+
(outputFile !== undefined
|
|
554
|
+
? `Output file: ${outputFile} (full output is appended there — Read it any time). `
|
|
555
|
+
: `Use TaskOutput("${taskId}") to read its output. `) +
|
|
556
|
+
`TaskStop("${taskId}") to stop it. ` +
|
|
557
|
+
(onNotify !== undefined
|
|
558
|
+
? `You will be notified when it completes — do not poll.`
|
|
559
|
+
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
560
|
+
adoptBudgetNote +
|
|
561
|
+
(tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
|
|
562
|
+
details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
autoBackgroundEligible: taskOpts.autoBackgroundOnTimeout === true && canAutoBackground(command),
|
|
566
|
+
}
|
|
567
|
+
: undefined;
|
|
568
|
+
let res;
|
|
569
|
+
try {
|
|
570
|
+
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, cwdRef, detachChain, taskOpts.execClamp, ctx.toolCallId, false);
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
taskOpts.detachHub?.gc(ctx.toolCallId);
|
|
574
|
+
}
|
|
575
|
+
const detached = typeof res !== "string" && res.details.detached === true;
|
|
576
|
+
const resText = typeof res === "string" ? res : typeof res.content === "string" ? res.content : undefined;
|
|
577
|
+
const hint = detached || resText === undefined ? undefined : ghRateLimitHint(command, resText);
|
|
578
|
+
const withHint = hint === undefined ? res : typeof res === "string" ? `${res}\n\n${hint}` : { ...res, content: `${resText}\n\n${hint}` };
|
|
579
|
+
if (typeof withHint === "string" || description === undefined)
|
|
580
|
+
return withHint;
|
|
581
|
+
if (withHint.details.detached)
|
|
582
|
+
return withHint;
|
|
583
|
+
return { ...withHint, details: { ...withHint.details, description } };
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp) {
|
|
588
|
+
const sample = [...allow].slice(0, 6).join(", ");
|
|
589
|
+
return defineTool({
|
|
590
|
+
name: "Bash",
|
|
591
|
+
contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
|
|
592
|
+
description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
|
|
593
|
+
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
594
|
+
"allowlisted commands run. Still subject to the deployment's approval policy.",
|
|
595
|
+
parameters: Type.Object({
|
|
596
|
+
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
597
|
+
timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${BASH_DEFAULT_TIMEOUT_MS}, max ${BASH_MAX_TIMEOUT_MS}).` })),
|
|
598
|
+
}),
|
|
599
|
+
effect: "read",
|
|
600
|
+
execute: async (args, ctx) => {
|
|
601
|
+
const { command, timeout } = args;
|
|
602
|
+
const reason = coarseReadonlyCheck(command, allow);
|
|
603
|
+
if (reason)
|
|
604
|
+
return errorResult(`Error (Bash): ${reason}`);
|
|
605
|
+
return runShell(env, rootCanonical, "Bash", command, msTimeoutToSec(timeout), ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
export function createEnvTaskOutputTool(env) {
|
|
610
|
+
return defineTool({
|
|
611
|
+
name: TASK_OUTPUT_TOOL_NAME,
|
|
612
|
+
aliases: [...TASK_OUTPUT_ALIASES],
|
|
613
|
+
contract: TASK_OUTPUT_CONTRACT,
|
|
614
|
+
description: composeTaskOutputDescription(ENV_DIRECT_TASK_TOOL_CAPS),
|
|
615
|
+
parameters: composeTaskOutputParams(ENV_DIRECT_TASK_TOOL_CAPS),
|
|
616
|
+
effect: "read",
|
|
617
|
+
execute: async (args) => {
|
|
618
|
+
const { task_id, bash_id, runId, filter } = args;
|
|
619
|
+
const id = resolveTaskIdArg(task_id, bash_id, runId);
|
|
620
|
+
if (!id)
|
|
621
|
+
return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
|
|
622
|
+
if (!hasBackgroundShell(env)) {
|
|
623
|
+
return errorResult("Error (TaskOutput): this environment does not support background processes.");
|
|
624
|
+
}
|
|
625
|
+
let rx;
|
|
626
|
+
if (filter !== undefined) {
|
|
627
|
+
try {
|
|
628
|
+
rx = new RegExp(filter);
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return errorResult(`Error (TaskOutput): invalid filter regex: ${filter}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const r = await env.pollBackground(id);
|
|
635
|
+
if (!r.ok)
|
|
636
|
+
return errorResult(`Error (TaskOutput): ${r.error.message}`);
|
|
637
|
+
const shape = (text) => clipShellOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text);
|
|
638
|
+
const status = r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status;
|
|
639
|
+
const dropped = r.value.bytesDroppedBeforeCursor ?? 0;
|
|
640
|
+
const droppedNote = dropped > 0 ? `[!] ${dropped} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n` : "";
|
|
641
|
+
const body = `status: ${status}\n${droppedNote}--- new stdout ---\n${shape(r.value.stdout) || "(none)"}\n` +
|
|
642
|
+
`--- new stderr ---\n${shape(r.value.stderr) || "(none)"}`;
|
|
643
|
+
return delimitUntrusted(`TaskOutput ${id}`, body);
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
export function createEnvTaskStopTool(env, registry = defaultTaskRegistry) {
|
|
648
|
+
return defineTool({
|
|
649
|
+
name: TASK_STOP_TOOL_NAME,
|
|
650
|
+
aliases: [...TASK_STOP_ALIASES],
|
|
651
|
+
contract: TASK_STOP_CONTRACT,
|
|
652
|
+
description: composeTaskStopDescription(ENV_DIRECT_TASK_TOOL_CAPS),
|
|
653
|
+
parameters: TASK_STOP_PARAMS,
|
|
654
|
+
effect: "write",
|
|
655
|
+
execute: async (args) => {
|
|
656
|
+
const { task_id, shell_id, runId } = args;
|
|
657
|
+
const id = resolveTaskIdArg(task_id, shell_id, runId);
|
|
658
|
+
if (!id)
|
|
659
|
+
return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
|
|
660
|
+
if (!hasBackgroundShell(env)) {
|
|
661
|
+
return errorResult("Error (TaskStop): this environment does not support background processes.");
|
|
662
|
+
}
|
|
663
|
+
registry.markStopSourceByShellId(id, "parent", env);
|
|
664
|
+
const r = await env.killBackground(id);
|
|
665
|
+
if (!r.ok) {
|
|
666
|
+
registry.clearPendingStopSourceByShellId(id, "parent", env);
|
|
667
|
+
return errorResult(`${id} is unknown or has already ended.`);
|
|
668
|
+
}
|
|
669
|
+
return `Terminated ${id}.`;
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|