@sema-agent/core 2.3.0 → 2.5.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/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +279 -128
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-compaction.d.ts +2 -0
- package/dist/core/auto-compaction.js +2 -1
- package/dist/core/context-edit.js +2 -1
- package/dist/core/mcp.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +5 -0
- package/dist/core/runner/prepare-task.js +41 -2
- package/dist/core/runner/runtask.js +39 -9
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +16 -5
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/session-reconcile.d.ts +1 -0
- package/dist/core/session-reconcile.js +40 -20
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +6 -0
- package/dist/core/task-registry-agent.js +24 -1
- package/dist/core/task-registry-monitor.js +6 -6
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry-shared.js +1 -1
- package/dist/core/task-registry.d.ts +8 -0
- package/dist/core/task-registry.js +59 -4
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/types.d.ts +10 -3
- package/dist/engine/compaction/compaction.d.ts +5 -0
- package/dist/engine/compaction/compaction.js +68 -2
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +53 -3
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/messages.js +11 -3
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/engine/session/import-validate.js +30 -1
- package/dist/engine/session/session.js +7 -5
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +41 -3
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +59 -22
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.js +18 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
|
@@ -104,6 +104,21 @@ export function workflowAgentCallKey(ordinal, spec, opts) {
|
|
|
104
104
|
};
|
|
105
105
|
return `${ordinal}:${boundInputHashOf(identity)}`;
|
|
106
106
|
}
|
|
107
|
+
function resolveChildSessionIdAtSpawn(spec) {
|
|
108
|
+
if (spec.sessionId)
|
|
109
|
+
return spec.sessionId;
|
|
110
|
+
if (spec.requireExistingSession === true || spec.resumeAt !== undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
return randomUUID();
|
|
113
|
+
}
|
|
114
|
+
export function assertSupportedAgentIsolation(isolation) {
|
|
115
|
+
if (isolation !== undefined && isolation !== "worktree") {
|
|
116
|
+
const shown = typeof isolation === "string" ? JSON.stringify(isolation) : String(isolation);
|
|
117
|
+
const e = new Error(`isolation ${shown} is not supported in workflow agents — only "worktree" (omit the option to run in the shared working tree). The agent was not started (fail-closed: an unrecognized isolation value must never silently run in the shared working tree).`);
|
|
118
|
+
e.code = "isolation.invalid";
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
107
122
|
function rethrowIfMaxAgents(e) {
|
|
108
123
|
if (e instanceof WorkflowMaxAgentsError)
|
|
109
124
|
throw e;
|
|
@@ -626,6 +641,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
626
641
|
};
|
|
627
642
|
return { tail, onActivity };
|
|
628
643
|
};
|
|
644
|
+
const createWorkspaceObserver = (rec) => (workspace) => {
|
|
645
|
+
if (finalized)
|
|
646
|
+
return;
|
|
647
|
+
if (!workspace.isolated || rec.worktreeDir === workspace.cwd)
|
|
648
|
+
return;
|
|
649
|
+
rec.worktreeDir = workspace.cwd;
|
|
650
|
+
void persist("update");
|
|
651
|
+
};
|
|
629
652
|
let currentPhase;
|
|
630
653
|
let openMarkerPhase;
|
|
631
654
|
let currentGroup;
|
|
@@ -700,6 +723,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
700
723
|
if ("error" in compiled)
|
|
701
724
|
throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
|
|
702
725
|
}
|
|
726
|
+
assertSupportedAgentIsolation(agentOpts.isolation);
|
|
703
727
|
const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
|
|
704
728
|
const phase = agentOpts.phase ?? currentPhase?.title;
|
|
705
729
|
const phaseInstance = resolveAgentPhase(agentOpts.phase);
|
|
@@ -799,7 +823,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
799
823
|
opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
|
|
800
824
|
}
|
|
801
825
|
: undefined;
|
|
802
|
-
const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label };
|
|
826
|
+
const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
|
|
803
827
|
let attempts = 0;
|
|
804
828
|
let throttleRetried = false;
|
|
805
829
|
let lastAttemptReason;
|
|
@@ -828,7 +852,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
828
852
|
const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
|
|
829
853
|
effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
830
854
|
armWatchdog();
|
|
831
|
-
const
|
|
855
|
+
const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
|
|
856
|
+
const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
|
|
857
|
+
if (attemptSessionId !== undefined && !finalized) {
|
|
858
|
+
rec.sessionId = attemptSessionId;
|
|
859
|
+
void persist("update");
|
|
860
|
+
}
|
|
832
861
|
const attemptInternals = {
|
|
833
862
|
...baseInternals,
|
|
834
863
|
...(bceSink !== undefined
|
|
@@ -1071,6 +1100,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1071
1100
|
if ("error" in compiled)
|
|
1072
1101
|
throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
|
|
1073
1102
|
}
|
|
1103
|
+
assertSupportedAgentIsolation(agentOpts.isolation);
|
|
1074
1104
|
const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
|
|
1075
1105
|
const phase = agentOpts.phase ?? currentPhase?.title;
|
|
1076
1106
|
const phaseInstance = resolveAgentPhase(agentOpts.phase);
|
|
@@ -1115,14 +1145,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1115
1145
|
rec.startedAt = now();
|
|
1116
1146
|
bceSpawn(callKey, label, agentOpts.agentType, false);
|
|
1117
1147
|
let stream;
|
|
1148
|
+
let childSessionId;
|
|
1118
1149
|
try {
|
|
1119
1150
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1120
1151
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
1121
1152
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
1122
1153
|
const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
|
|
1123
|
-
const
|
|
1154
|
+
const baseRunSpec = agentOpts.schema
|
|
1124
1155
|
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1125
1156
|
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1157
|
+
childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
|
|
1158
|
+
const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
|
|
1126
1159
|
const enrichedForwardS = opts.onForwardEvent !== undefined
|
|
1127
1160
|
? (e) => {
|
|
1128
1161
|
opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
|
|
@@ -1135,6 +1168,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1135
1168
|
...(enrichedForwardS !== undefined ? { onForwardEvent: enrichedForwardS } : {}),
|
|
1136
1169
|
agentName: label,
|
|
1137
1170
|
onActivity,
|
|
1171
|
+
onWorkspaceResolved: createWorkspaceObserver(rec),
|
|
1138
1172
|
...(bceSink !== undefined
|
|
1139
1173
|
? {
|
|
1140
1174
|
onForwardEvent: (e) => {
|
|
@@ -1157,6 +1191,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1157
1191
|
bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
|
|
1158
1192
|
throw err;
|
|
1159
1193
|
}
|
|
1194
|
+
if (childSessionId !== undefined && !finalized) {
|
|
1195
|
+
rec.sessionId = childSessionId;
|
|
1196
|
+
void persist("update");
|
|
1197
|
+
}
|
|
1160
1198
|
const steer = async (content) => {
|
|
1161
1199
|
const marker = `steer-${++steerMarkerSeq}`;
|
|
1162
1200
|
const framed = `[operator steer ${marker}] An operator/leader sent guidance for your task. Take it into account on your NEXT step. ` +
|
|
@@ -26,7 +26,13 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
26
26
|
execClamp?: ExecClampOption;
|
|
27
27
|
autoBackgroundOnTimeout?: boolean;
|
|
28
28
|
oneShot?: boolean;
|
|
29
|
+
additionalRoots?: readonly string[];
|
|
30
|
+
bashDefaultTimeoutMs?: number;
|
|
31
|
+
bashMaxTimeoutMs?: number;
|
|
32
|
+
}): AgentTool;
|
|
33
|
+
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, timeoutOpts?: {
|
|
34
|
+
bashDefaultTimeoutMs?: number;
|
|
35
|
+
bashMaxTimeoutMs?: number;
|
|
29
36
|
}): AgentTool;
|
|
30
|
-
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption): AgentTool;
|
|
31
37
|
export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
|
|
32
38
|
export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -5,9 +5,9 @@ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_S
|
|
|
5
5
|
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
6
6
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
7
7
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
8
|
-
import { imageMagicMatches } from "./safety.js";
|
|
8
|
+
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
9
9
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
11
11
|
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
|
|
12
12
|
export function bashReversibilityProbe(allow) {
|
|
13
13
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
@@ -156,9 +156,15 @@ export function canAutoBackground(command) {
|
|
|
156
156
|
return false;
|
|
157
157
|
return true;
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
function msTimeoutToRequestedSec(timeoutMs) {
|
|
160
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs))
|
|
161
|
+
return undefined;
|
|
162
|
+
return Math.max(1, Math.round(timeoutMs / 1000));
|
|
163
|
+
}
|
|
164
|
+
async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, cwdRef, detach, execClamp, toolCallId, readOnly, containmentRoots) {
|
|
165
|
+
const requestedSec = Math.max(1, Math.floor(timeoutSec ?? caps.defaultSec));
|
|
166
|
+
let timeout = Math.min(caps.maxSec, requestedSec);
|
|
167
|
+
const cappedByMaxTimeout = requestedSec > caps.maxSec;
|
|
162
168
|
let clampedByDeadline = false;
|
|
163
169
|
let deadlineExhausted = false;
|
|
164
170
|
if (execClamp) {
|
|
@@ -167,7 +173,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
167
173
|
const rawRemainSec = Math.floor((deadline - Date.now()) / 1000);
|
|
168
174
|
const maxSec = Math.max(1, rawRemainSec);
|
|
169
175
|
if (maxSec < timeout) {
|
|
170
|
-
execClamp.onClamp?.(
|
|
176
|
+
execClamp.onClamp?.(requestedSec, maxSec);
|
|
171
177
|
timeout = maxSec;
|
|
172
178
|
clampedByDeadline = true;
|
|
173
179
|
deadlineExhausted = rawRemainSec < 1;
|
|
@@ -235,13 +241,16 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
235
241
|
? deadlineExhausted
|
|
236
242
|
? `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
243
|
: `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
|
-
:
|
|
244
|
+
: cappedByMaxTimeout
|
|
245
|
+
? `Command timed out after ${timeout}s — requested ${requestedSec}s, capped at ${caps.maxSec}s (engine ceiling: requests above ${caps.maxSec}s are reduced to it). The process was killed; output produced before the cutoff is shown below. Re-running with a larger timeout gets the same ${caps.maxSec}s ceiling — use run_in_background for work that needs longer, a narrower command, or resume from the partial progress below.`
|
|
246
|
+
: `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
247
|
: res.error.code === "aborted"
|
|
240
248
|
? `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`
|
|
241
249
|
: `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
250
|
const captured = (pStdout ? `--- partial stdout ---\n${pStdout}` : "") +
|
|
243
251
|
(pStderr ? `${pStdout ? "\n" : ""}--- partial stderr ---\n${pStderr}` : "");
|
|
244
|
-
const
|
|
252
|
+
const zeroOutput = captured.length === 0;
|
|
253
|
+
const body = !zeroOutput
|
|
245
254
|
? `\n${delimitUntrusted("partial command output", captured)}`
|
|
246
255
|
: `\n(no output was produced before the cutoff)`;
|
|
247
256
|
const overflowNote = cutOverflowFile !== undefined ? shellRecoveryHint(cutOverflowFile, readOnly) : "";
|
|
@@ -257,12 +266,15 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
257
266
|
? {
|
|
258
267
|
timedOut: true,
|
|
259
268
|
timeoutSec: timeout,
|
|
260
|
-
...(clampedByDeadline ? { requestedTimeoutSec: requestedSec
|
|
269
|
+
...(clampedByDeadline || cappedByMaxTimeout ? { requestedTimeoutSec: requestedSec } : {}),
|
|
270
|
+
...(clampedByDeadline ? { clampedByDeadline: true } : {}),
|
|
271
|
+
...(cappedByMaxTimeout ? { cappedToMaxTimeoutSec: caps.maxSec } : {}),
|
|
261
272
|
}
|
|
262
273
|
: res.error.code === "aborted"
|
|
263
274
|
? { aborted: true }
|
|
264
275
|
: { callbackError: true }),
|
|
265
276
|
},
|
|
277
|
+
...(zeroOutput ? { isError: true } : {}),
|
|
266
278
|
};
|
|
267
279
|
}
|
|
268
280
|
return {
|
|
@@ -291,6 +303,16 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
291
303
|
cwdRef.current = captured;
|
|
292
304
|
}
|
|
293
305
|
}
|
|
306
|
+
let cwdOutsideNote = "";
|
|
307
|
+
if (cwdRef && containmentRoots !== undefined && containmentRoots.length > 0 && !withinAnyRoot(containmentRoots, cwdRef.current)) {
|
|
308
|
+
const cwdCanon = await env.canonicalPath(cwdRef.current, signal);
|
|
309
|
+
if (!(cwdCanon.ok && withinAnyRoot(containmentRoots, cwdCanon.value))) {
|
|
310
|
+
cwdOutsideNote = `NOTE: working directory is now outside the task root(s) (${cwdRef.current}); relative paths in the structured file tools (Read/Edit/Write/Grep/Glob) will resolve there and may be refused. Pass absolute in-root paths, or \`cd\` back inside.`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const survivedCapNote = cappedByMaxTimeout
|
|
314
|
+
? `NOTE: requested timeout ${requestedSec}s was capped to the ${caps.maxSec}s engine ceiling (requests above the max are reduced to it) — no effect on this run (the command completed in time); use run_in_background for work that needs longer.`
|
|
315
|
+
: "";
|
|
294
316
|
const clippedStdout = clipShellOutput(stdout);
|
|
295
317
|
const clippedStderr = clipShellOutput(stderr);
|
|
296
318
|
const stdoutImage = dataUriImageFromStdout(stdout);
|
|
@@ -300,9 +322,9 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
300
322
|
return {
|
|
301
323
|
content: [
|
|
302
324
|
{ 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}` },
|
|
325
|
+
{ type: "text", text: `exit code: ${exitCode}\n[Image data detected in stdout and shown above]${stderr.trim() !== "" ? `\n--- stderr ---\n${clippedStderr}` : ""}${imgOverflowNote}${cwdOutsideNote ? `\n${cwdOutsideNote}` : ""}${survivedCapNote ? `\n${survivedCapNote}` : ""}` },
|
|
304
326
|
],
|
|
305
|
-
details: { type: "bash", stdout: "[image data]", stderr: clippedStderr, exitCode, isImage: true, ...(imgOverflowFile !== undefined ? { output_file: imgOverflowFile } : {}) },
|
|
327
|
+
details: { type: "bash", stdout: "[image data]", stderr: clippedStderr, exitCode, isImage: true, ...(imgOverflowFile !== undefined ? { output_file: imgOverflowFile } : {}), ...(cappedByMaxTimeout ? { requestedTimeoutSec: requestedSec, cappedToMaxTimeoutSec: caps.maxSec } : {}) },
|
|
306
328
|
};
|
|
307
329
|
}
|
|
308
330
|
const cleanOverflowFile = stdout.length > bashMaxOutputChars() || stderr.length > bashMaxOutputChars() ? await writeShellOverflowFile(env, stdout, stderr) : undefined;
|
|
@@ -313,6 +335,10 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
313
335
|
parts.push(`--- stderr ---\n${clippedStderr}`);
|
|
314
336
|
if (overflowNote)
|
|
315
337
|
parts.push(overflowNote.trim());
|
|
338
|
+
if (cwdOutsideNote)
|
|
339
|
+
parts.push(cwdOutsideNote);
|
|
340
|
+
if (survivedCapNote)
|
|
341
|
+
parts.push(survivedCapNote);
|
|
316
342
|
return {
|
|
317
343
|
content: parts.join("\n"),
|
|
318
344
|
details: {
|
|
@@ -322,10 +348,11 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
|
|
|
322
348
|
exitCode,
|
|
323
349
|
...(exit1Note ? { returnCodeInterpretation: exit1Note } : {}),
|
|
324
350
|
...(cleanOverflowFile !== undefined ? { output_file: cleanOverflowFile } : {}),
|
|
351
|
+
...(cappedByMaxTimeout ? { requestedTimeoutSec: requestedSec, cappedToMaxTimeoutSec: caps.maxSec } : {}),
|
|
325
352
|
},
|
|
326
353
|
};
|
|
327
354
|
}
|
|
328
|
-
function bashDescription(coAuthor, bgNotifies = false, bgRetained = false) {
|
|
355
|
+
function bashDescription(coAuthor, caps, bgNotifies = false, bgRetained = false) {
|
|
329
356
|
const coAuthorLines = coAuthor === false
|
|
330
357
|
? "- Follow the deployment's commit-message conventions."
|
|
331
358
|
: `- End git commit messages with:\nCo-Authored-By: ${coAuthor}`;
|
|
@@ -334,7 +361,7 @@ function bashDescription(coAuthor, bgNotifies = false, bgRetained = false) {
|
|
|
334
361
|
- 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
362
|
- 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
363
|
- 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 ${
|
|
364
|
+
- \`timeout\` is in milliseconds: default ${caps.defaultMs}, max ${caps.maxMs}.
|
|
338
365
|
- \`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
366
|
|
|
340
367
|
# Git
|
|
@@ -348,15 +375,17 @@ ${coAuthorLines}
|
|
|
348
375
|
- 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
376
|
}
|
|
350
377
|
export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
|
|
378
|
+
const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
|
|
379
|
+
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
351
380
|
const bgNotifies = taskOpts.taskRegistry !== undefined && taskOpts.onTaskNotification !== undefined;
|
|
352
381
|
const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
353
382
|
return defineTool({
|
|
354
383
|
name: "Bash",
|
|
355
384
|
contract: { contractId: "core.bash@1", implementationRevision: "1" },
|
|
356
|
-
description: bashDescription(coAuthor, bgNotifies, bgRetained),
|
|
385
|
+
description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained),
|
|
357
386
|
parameters: Type.Object({
|
|
358
387
|
command: Type.String({ description: "The command to execute" }),
|
|
359
|
-
timeout: Type.Optional(Type.Number({ description: `Optional timeout in milliseconds (max ${
|
|
388
|
+
timeout: Type.Optional(Type.Number({ description: `Optional timeout in milliseconds (max ${timeoutCaps.maxMs}; requests above the max are capped to it)` })),
|
|
360
389
|
description: Type.Optional(Type.String({
|
|
361
390
|
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
391
|
"\n" +
|
|
@@ -406,7 +435,12 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
406
435
|
const appliedTimeoutSec = typeof bgCaps.defaultBgTimeoutSec === "number" && typeof bgCaps.maxBgTimeoutSec === "number"
|
|
407
436
|
? Math.min(requestedTimeoutSec ?? bgCaps.defaultBgTimeoutSec, bgCaps.maxBgTimeoutSec)
|
|
408
437
|
: undefined;
|
|
409
|
-
const
|
|
438
|
+
const bgCappedByMax = appliedTimeoutSec !== undefined && requestedTimeoutSec !== undefined && typeof bgCaps.maxBgTimeoutSec === "number" && requestedTimeoutSec > bgCaps.maxBgTimeoutSec;
|
|
439
|
+
const budgetNote = appliedTimeoutSec !== undefined
|
|
440
|
+
? bgCappedByMax
|
|
441
|
+
? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — auto-terminates if still running after ${appliedTimeoutSec}s.`
|
|
442
|
+
: ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
|
|
443
|
+
: "";
|
|
410
444
|
const lifetimeNote = bgCaps.retainBackgroundProcesses === true
|
|
411
445
|
? ""
|
|
412
446
|
: ` 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).`;
|
|
@@ -462,7 +496,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
462
496
|
? ` Output file: ${outputFile} (full output is appended there — Read it any time).`
|
|
463
497
|
: ` Use TaskOutput("${taskId}") to check interim output.`;
|
|
464
498
|
if (taskOpts.oneShot === true && onNotify !== undefined) {
|
|
465
|
-
return (`Command running in background; task_id=${taskId}.${interimNote} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput("${taskId}",
|
|
499
|
+
return (`Command running in background; task_id=${taskId}.${interimNote} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput({ task_id: "${taskId}", block: true }). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget. TaskStop("${taskId}") to stop.` +
|
|
466
500
|
budgetNote +
|
|
467
501
|
lifetimeNote);
|
|
468
502
|
}
|
|
@@ -560,7 +594,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
560
594
|
: `Use TaskOutput("${taskId}") to read its output. `) +
|
|
561
595
|
`TaskStop("${taskId}") to stop it. ` +
|
|
562
596
|
(taskOpts.oneShot === true && onNotify !== undefined
|
|
563
|
-
?
|
|
597
|
+
?
|
|
598
|
+
`This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput({ task_id: "${taskId}", block: true }).`
|
|
564
599
|
: onNotify !== undefined
|
|
565
600
|
? `You will be notified when it completes — do not poll.`
|
|
566
601
|
: `Poll TaskOutput until its status is no longer "running".`) +
|
|
@@ -574,7 +609,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
574
609
|
: undefined;
|
|
575
610
|
let res;
|
|
576
611
|
try {
|
|
577
|
-
res = await runShell(env, rootCanonical, "Bash", command,
|
|
612
|
+
res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, cwdRef, detachChain, taskOpts.execClamp, ctx.toolCallId, false, [rootCanonical, ...(taskOpts.additionalRoots ?? [])]);
|
|
578
613
|
}
|
|
579
614
|
finally {
|
|
580
615
|
taskOpts.detachHub?.gc(ctx.toolCallId);
|
|
@@ -591,7 +626,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
591
626
|
},
|
|
592
627
|
});
|
|
593
628
|
}
|
|
594
|
-
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp) {
|
|
629
|
+
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, timeoutOpts) {
|
|
630
|
+
const timeoutCaps = resolveBashTimeoutCaps(timeoutOpts);
|
|
631
|
+
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
595
632
|
const sample = [...allow].slice(0, 6).join(", ");
|
|
596
633
|
return defineTool({
|
|
597
634
|
name: "Bash",
|
|
@@ -601,7 +638,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp) {
|
|
|
601
638
|
"allowlisted commands run. Still subject to the deployment's approval policy.",
|
|
602
639
|
parameters: Type.Object({
|
|
603
640
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
604
|
-
timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${
|
|
641
|
+
timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
|
|
605
642
|
}),
|
|
606
643
|
effect: "read",
|
|
607
644
|
execute: async (args, ctx) => {
|
|
@@ -609,7 +646,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp) {
|
|
|
609
646
|
const reason = coarseReadonlyCheck(command, allow);
|
|
610
647
|
if (reason)
|
|
611
648
|
return errorResult(`Error (Bash): ${reason}`);
|
|
612
|
-
return runShell(env, rootCanonical, "Bash", command,
|
|
649
|
+
return runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
613
650
|
},
|
|
614
651
|
});
|
|
615
652
|
}
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { sha256, resolveKey, violationText, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
|
|
3
|
+
import { sha256, resolveKey, violationText, violationDetails, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
|
|
4
4
|
import { decodeTextBytes } from "./encoding.js";
|
|
5
5
|
import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
|
|
6
6
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
|
|
@@ -38,8 +38,8 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
38
38
|
"- Do NOT re-read a file you just edited to verify — Edit/Write would have errored if the change failed, and the harness tracks file state for you.",
|
|
39
39
|
parameters: Type.Object({
|
|
40
40
|
...FILE_PATH_PARAMS,
|
|
41
|
-
offset: Type.Optional(Type.
|
|
42
|
-
limit: Type.Optional(Type.
|
|
41
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "The line number to start reading from. Only provide if the file is too large to read at once (1-based; default 1)." })),
|
|
42
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, description: "The number of lines to read. Only provide if the file is too large to read at once. (Default: the whole file, bounded by the output-token cap.)" })),
|
|
43
43
|
pages: Type.Optional(Type.String({ description: `Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.` })),
|
|
44
44
|
}),
|
|
45
45
|
effect: "read",
|
|
@@ -48,9 +48,13 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
48
48
|
const path = fileArgPath(args);
|
|
49
49
|
if (path === undefined)
|
|
50
50
|
return errorResult(`Error (Read): file_path is required.`);
|
|
51
|
+
const fileUnchangedResult = (text, reason, startLine, endLine, totalLines) => ({
|
|
52
|
+
content: text,
|
|
53
|
+
details: { type: "file_unchanged", reason, file: { filePath: path, startLine, endLine, totalLines } },
|
|
54
|
+
});
|
|
51
55
|
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx));
|
|
52
56
|
if (!r.ok)
|
|
53
|
-
return errorResult(violationText("Read", r.violation));
|
|
57
|
+
return errorResult(violationText("Read", r.violation), violationDetails(r.violation));
|
|
54
58
|
const isNb = isNotebookPath(r.key);
|
|
55
59
|
const imageMime = imageMimeForRead(r.key);
|
|
56
60
|
if (imageMime !== undefined) {
|
|
@@ -116,20 +120,23 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
116
120
|
if (r.key.toLowerCase().endsWith(".pdf")) {
|
|
117
121
|
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities));
|
|
118
122
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
123
|
+
const binaryExt = hasBinaryExtension(r.key);
|
|
124
|
+
const binaryExtRefusal = () => errorResult(`Error (Read): "${path}" appears to be a binary file (by extension); this tool reads UTF-8 text only.`);
|
|
122
125
|
const info = await env.fileInfo(r.key, ctx.signal);
|
|
123
126
|
if (info.ok) {
|
|
124
127
|
if (info.value.kind === "directory") {
|
|
125
128
|
return errorResult(`Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`);
|
|
126
129
|
}
|
|
127
130
|
if (info.value.size > SLICED_READ_MAX_BYTES) {
|
|
131
|
+
if (binaryExt)
|
|
132
|
+
return binaryExtRefusal();
|
|
128
133
|
return errorResult(`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
|
|
129
134
|
`(${info.value.size} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
|
|
130
135
|
`Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
|
|
131
136
|
}
|
|
132
137
|
if (!isNb && info.value.size > MAX_READ_BYTES && limit === undefined) {
|
|
138
|
+
if (binaryExt)
|
|
139
|
+
return binaryExtRefusal();
|
|
133
140
|
return errorResult(`Error (Read): "${path}" is too large to read in full (${info.value.size} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
|
|
134
141
|
}
|
|
135
142
|
}
|
|
@@ -143,11 +150,15 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
143
150
|
return errorResult(`Error (Read): cannot read "${path}": ${readBin.error.message}`);
|
|
144
151
|
const readSize = readBin.value.byteLength;
|
|
145
152
|
if (readSize > SLICED_READ_MAX_BYTES) {
|
|
153
|
+
if (binaryExt)
|
|
154
|
+
return binaryExtRefusal();
|
|
146
155
|
return errorResult(`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
|
|
147
156
|
`(${readSize} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
|
|
148
157
|
`Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
|
|
149
158
|
}
|
|
150
159
|
if (!isNb && readSize > MAX_READ_BYTES && limit === undefined) {
|
|
160
|
+
if (binaryExt)
|
|
161
|
+
return binaryExtRefusal();
|
|
151
162
|
return errorResult(`Error (Read): "${path}" is too large to read in full (${readSize} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
|
|
152
163
|
}
|
|
153
164
|
if (pdfMagicMatches(readBin.value)) {
|
|
@@ -194,10 +205,10 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
194
205
|
}
|
|
195
206
|
const prevNb = state.get(r.key);
|
|
196
207
|
if (prevNb?.seededFromContext && !prevNb.isPartialView && prevNb.hash === hash) {
|
|
197
|
-
return seededFileUnchangedReminder(r.key);
|
|
208
|
+
return fileUnchangedResult(seededFileUnchangedReminder(r.key), "already-in-context", 1, total, total);
|
|
198
209
|
}
|
|
199
210
|
if (prevNb && !prevNb.isPartialView && prevNb.hash === hash && prevNb.view && prevNb.view.start === 1 && prevNb.view.end === total) {
|
|
200
|
-
return `[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]
|
|
211
|
+
return fileUnchangedResult(`[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]`, "unchanged-since-last-read", 1, total, total);
|
|
201
212
|
}
|
|
202
213
|
state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
|
|
203
214
|
const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
|
|
@@ -251,10 +262,10 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
251
262
|
const truncated = start > 1 || end < total || pageMarker !== undefined;
|
|
252
263
|
const prev = state.get(r.key);
|
|
253
264
|
if (total > 0 && prev?.seededFromContext && !prev.isPartialView && start === 1 && effLimit === undefined && prev.hash === hash) {
|
|
254
|
-
return seededFileUnchangedReminder(r.key);
|
|
265
|
+
return fileUnchangedResult(seededFileUnchangedReminder(r.key), "already-in-context", 1, total, total);
|
|
255
266
|
}
|
|
256
267
|
if (total > 0 && prev && !prev.isPartialView && prev.hash === hash && prev.view && prev.view.start === start && prev.view.end === end) {
|
|
257
|
-
return `[${path}: unchanged since you last read it (lines ${start}-${end} of ${total}); content omitted to save context]
|
|
268
|
+
return fileUnchangedResult(`[${path}: unchanged since you last read it (lines ${start}-${end} of ${total}); content omitted to save context]`, "unchanged-since-last-read", start, end, total);
|
|
258
269
|
}
|
|
259
270
|
state.set(r.key, {
|
|
260
271
|
hash,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { resolveKey, violationText } from "./safety.js";
|
|
3
|
+
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
|
|
5
5
|
export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
6
6
|
return defineTool({
|
|
@@ -63,7 +63,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
63
63
|
if (a.path !== undefined) {
|
|
64
64
|
const r = await resolveKey(env, rootCanonical, a.path, ctx.signal, rootCanonical, additionalRoots);
|
|
65
65
|
if (!r.ok)
|
|
66
|
-
return errorResult(violationText("Grep", r.violation));
|
|
66
|
+
return errorResult(violationText("Grep", r.violation), violationDetails(r.violation));
|
|
67
67
|
scoped = r.key;
|
|
68
68
|
}
|
|
69
69
|
const grepRun = await runGrepDetailed(env, rootCanonical, {
|
|
@@ -182,7 +182,7 @@ export function createGlobTool(env, rootCanonical, additionalRoots) {
|
|
|
182
182
|
if (path !== undefined) {
|
|
183
183
|
const r = await resolveKey(env, rootCanonical, path, ctx.signal, rootCanonical, additionalRoots);
|
|
184
184
|
if (!r.ok)
|
|
185
|
-
return errorResult(violationText("Glob", r.violation));
|
|
185
|
+
return errorResult(violationText("Glob", r.violation), violationDetails(r.violation));
|
|
186
186
|
scoped = r.key;
|
|
187
187
|
}
|
|
188
188
|
const r2 = await runGlobDetailed(env, rootCanonical, pattern, { path: scoped, max: max_results }, ctx.signal);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
3
|
-
import { type ReadFileState } from "./safety.js";
|
|
3
|
+
import { type FsViolation, type ReadFileState } from "./safety.js";
|
|
4
4
|
import { type DecodedTextFile } from "./encoding.js";
|
|
5
5
|
import { type ImageDownsampler } from "../../core/mcp.js";
|
|
6
6
|
export declare const MAX_READ_BYTES: number;
|
|
@@ -15,11 +15,7 @@ export declare function decodeEditBytes(bytes: Uint8Array, path: string): {
|
|
|
15
15
|
message: string;
|
|
16
16
|
};
|
|
17
17
|
export declare function persistedTextOf(encoded: string | Uint8Array): string;
|
|
18
|
-
export declare function notReadRefusalText(env: ExecutionEnv, toolName: string, key: string, v:
|
|
19
|
-
code: string;
|
|
20
|
-
message: string;
|
|
21
|
-
partialView?: boolean;
|
|
22
|
-
}, signal?: AbortSignal, fallbackHint?: string): Promise<string>;
|
|
18
|
+
export declare function notReadRefusalText(env: ExecutionEnv, toolName: string, key: string, v: Pick<FsViolation, "code" | "message" | "partialView">, signal?: AbortSignal, fallbackHint?: string): Promise<string>;
|
|
23
19
|
export declare const MAX_IMAGE_READ_BYTES: number;
|
|
24
20
|
export declare const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES: number;
|
|
25
21
|
export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
|
|
@@ -30,6 +26,20 @@ export declare const BASH_DEFAULT_TIMEOUT_SEC = 120;
|
|
|
30
26
|
export declare const BASH_MAX_TIMEOUT_SEC = 600;
|
|
31
27
|
export declare const BASH_DEFAULT_TIMEOUT_MS: number;
|
|
32
28
|
export declare const BASH_MAX_TIMEOUT_MS: number;
|
|
29
|
+
export declare function resolveBashTimeoutCaps(opts?: {
|
|
30
|
+
bashDefaultTimeoutMs?: number;
|
|
31
|
+
bashMaxTimeoutMs?: number;
|
|
32
|
+
}): {
|
|
33
|
+
defaultMs: number;
|
|
34
|
+
maxMs: number;
|
|
35
|
+
};
|
|
36
|
+
export declare function bashTimeoutCapsSec(caps: {
|
|
37
|
+
defaultMs: number;
|
|
38
|
+
maxMs: number;
|
|
39
|
+
}): {
|
|
40
|
+
defaultSec: number;
|
|
41
|
+
maxSec: number;
|
|
42
|
+
};
|
|
33
43
|
export declare function bashMaxOutputChars(): number;
|
|
34
44
|
export declare const FILE_PATH_PARAMS: {
|
|
35
45
|
file_path: Type.TOptional<Type.TString>;
|
|
@@ -40,7 +50,10 @@ export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string
|
|
|
40
50
|
export declare function shellRecoveryHint(path: string, readOnly: boolean | undefined): string;
|
|
41
51
|
export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
|
|
42
52
|
export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
43
|
-
export declare function msTimeoutToSec(timeoutMs: number | undefined
|
|
53
|
+
export declare function msTimeoutToSec(timeoutMs: number | undefined, caps?: {
|
|
54
|
+
defaultMs: number;
|
|
55
|
+
maxMs: number;
|
|
56
|
+
}): number;
|
|
44
57
|
export declare function ipynbRedirect(toolName: string, path: string): string | undefined;
|
|
45
58
|
export declare function countLines(s: string): number;
|
|
46
59
|
export declare function seededFileUnchangedReminder(filePath: string): string;
|
|
@@ -51,6 +51,19 @@ export const BASH_DEFAULT_TIMEOUT_SEC = 120;
|
|
|
51
51
|
export const BASH_MAX_TIMEOUT_SEC = 600;
|
|
52
52
|
export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
|
|
53
53
|
export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
|
|
54
|
+
function validTimeoutMs(n) {
|
|
55
|
+
return n !== undefined && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function resolveBashTimeoutCaps(opts) {
|
|
58
|
+
const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
|
|
59
|
+
validTimeoutMs(Number(process.env.BASH_DEFAULT_TIMEOUT_MS)) ??
|
|
60
|
+
BASH_DEFAULT_TIMEOUT_MS;
|
|
61
|
+
const maxMs = Math.max(validTimeoutMs(opts?.bashMaxTimeoutMs) ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS)) ?? BASH_MAX_TIMEOUT_MS, defaultMs);
|
|
62
|
+
return { defaultMs, maxMs };
|
|
63
|
+
}
|
|
64
|
+
export function bashTimeoutCapsSec(caps) {
|
|
65
|
+
return { defaultSec: Math.max(1, Math.round(caps.defaultMs / 1000)), maxSec: Math.max(1, Math.round(caps.maxMs / 1000)) };
|
|
66
|
+
}
|
|
54
67
|
const BASH_DEFAULT_MAX_OUTPUT_CHARS = 30_000;
|
|
55
68
|
const BASH_MAX_OUTPUT_CHARS_CEILING = 150_000;
|
|
56
69
|
export function bashMaxOutputChars() {
|
|
@@ -84,10 +97,11 @@ export function shellRecoveryHint(path, readOnly) {
|
|
|
84
97
|
}
|
|
85
98
|
export const FILE_STATE_TRAILER = " (file state is current in your context — no need to Read it back)";
|
|
86
99
|
export const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
87
|
-
export function msTimeoutToSec(timeoutMs) {
|
|
100
|
+
export function msTimeoutToSec(timeoutMs, caps = resolveBashTimeoutCaps()) {
|
|
101
|
+
const { defaultSec, maxSec } = bashTimeoutCapsSec(caps);
|
|
88
102
|
if (timeoutMs === undefined || !Number.isFinite(timeoutMs))
|
|
89
|
-
return
|
|
90
|
-
return Math.min(
|
|
103
|
+
return defaultSec;
|
|
104
|
+
return Math.min(maxSec, Math.max(1, Math.round(timeoutMs / 1000)));
|
|
91
105
|
}
|
|
92
106
|
export function ipynbRedirect(toolName, path) {
|
|
93
107
|
if (path.toLowerCase().endsWith(".ipynb")) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
4
|
-
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT, } from "./safety.js";
|
|
4
|
+
import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT, } from "./safety.js";
|
|
5
5
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
|
|
6
6
|
import { MAX_EDIT_BYTES, formatByteSize, decodeEditBytes, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
|
|
7
7
|
async function gateToolWrite(hook, tool, path, key, content) {
|
|
@@ -56,7 +56,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
56
56
|
return errorResult(`Error (Edit): file_path is required.`);
|
|
57
57
|
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
58
58
|
if (!r.ok)
|
|
59
|
-
return errorResult(violationText("Edit", r.violation));
|
|
59
|
+
return errorResult(violationText("Edit", r.violation), violationDetails(r.violation));
|
|
60
60
|
if (!batch && a.old_string === a.new_string) {
|
|
61
61
|
return errorResult(violationText("Edit", { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." }));
|
|
62
62
|
}
|
|
@@ -241,7 +241,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
241
241
|
return errorResult(ipynb);
|
|
242
242
|
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
243
243
|
if (!r.ok)
|
|
244
|
-
return errorResult(violationText("Write", r.violation));
|
|
244
|
+
return errorResult(violationText("Write", r.violation), violationDetails(r.violation));
|
|
245
245
|
const exists = await env.exists(r.key, ctx.signal);
|
|
246
246
|
if (!exists.ok)
|
|
247
247
|
return errorResult(`Error (Write): cannot stat "${path}": ${exists.error.message}`);
|
|
@@ -331,7 +331,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
331
331
|
return errorResult(`Error (NotebookEdit): cell_id is required for replace/delete.`);
|
|
332
332
|
const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
333
333
|
if (!r.ok)
|
|
334
|
-
return errorResult(violationText("NotebookEdit", r.violation));
|
|
334
|
+
return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
|
|
335
335
|
const notRead = requireRead(state, r.key);
|
|
336
336
|
if (notRead)
|
|
337
337
|
return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal));
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface HandsToolkitOptions {
|
|
|
29
29
|
}) => void;
|
|
30
30
|
detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
|
|
31
31
|
execClamp?: ExecClampOption;
|
|
32
|
+
bashDefaultTimeoutMs?: number;
|
|
33
|
+
bashMaxTimeoutMs?: number;
|
|
32
34
|
oneShot?: boolean;
|
|
33
35
|
autoBackgroundOnTimeout?: boolean;
|
|
34
36
|
readImageDownsampler?: ReadImageDownsamplerOption;
|