Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/agent-gui/src/lib/tools/shellTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,32 @@ export function createShellTools(params: {
stdout: string;
stderr: string;
shellFamily?: string;
/** 本次运行是否被 shell 超时杀掉(`ShellRunResponse.timed_out`)。 */
timedOut?: boolean;
/** 实际生效的超时上限(res.effective_timeout_ms,回退到请求值)。 */
effectiveTimeoutMs?: number;
/** 当前 provider 的硬上限;等于 effectiveTimeoutMs 时说明是策略上限而非请求值。 */
timeoutCapMs?: number;
providerLabel?: string;
}) {
const combined = [params.command, params.stdout, params.stderr].join("\n");
const hints: string[] = [];

if (params.timedOut) {
// 报告里的真实困惑:"同一个构建为什么连续跑了三次、日志里看不到失败原因"。
// 超时杀进程时,命令自己的日志文件里当然什么都没有——它被外部 SIGKILL 了。
// 所以这里把"被杀的原因"写成模型和人都能直接读到的一行,并说明重跑同一
// 条命令不会有别的结果(这一层从来没有自动重试,重复执行都是模型自己发的)。
const limitMs = params.effectiveTimeoutMs ?? 0;
const capMs = params.timeoutCapMs ?? limitMs;
hints.push(
`Hint: This run was killed by the shell timeout (${limitMs}ms) — it did not crash, and the command itself reported no error. ` +
`The partial output above is everything it produced before the kill. ` +
`${params.providerLabel ?? "This provider"} caps Bash at ${capMs}ms, so re-running the identical command unchanged reaches the same limit: ` +
`redirect the output to a file and poll that file, split the work into steps that finish within ${capMs}ms, or pass an explicit timeout_ms (clamped to ${capMs}ms).`,
);
}

if (
runtimePlatform === "windows" &&
(params.shellFamily === "powershell" || params.shellFamily === "cmd")
Expand Down Expand Up @@ -1582,6 +1604,10 @@ export function createShellTools(params: {
stdout: res.stdout || "",
stderr: res.stderr || "",
shellFamily: res.shell_family,
timedOut: Boolean(res.timed_out),
effectiveTimeoutMs: res.effective_timeout_ms || timeout_ms,
timeoutCapMs: timeoutPolicy.maxTimeoutMs,
providerLabel: timeoutPolicy.providerLabel,
})
: "";

Expand Down
51 changes: 51 additions & 0 deletions crates/agent-gui/test/tools/shell-tools.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1520,3 +1520,54 @@ test("resumable Bash blocks leading sleep polling but allows short or internal s
);
assert.equal(calls.length, 2);
});

test("Bash tool spells out a provider-capped timeout instead of leaving the kill unexplained", async () => {
// 实测报告里的困惑:"同一个构建连续跑了三次,前两次日志里看不到失败原因"。
// 这一层从来没有自动重试——重复执行都是模型自己发起的;前两次是被 provider
// 的 30s 上限杀掉的,而被外部杀死的命令自己的日志里当然什么都没有。所以工具
// 结果必须把"这是超时被杀,不是崩溃/脚本报错"写清楚,并给出可行替代做法。
const loader = createTsModuleLoader({
mocks: {
"@tauri-apps/api/core": {
async invoke(command) {
assert.equal(command, "shell_run");
return {
exit_code: -1,
shell: "bash",
platform: "macos",
profile: "posix-bash",
shell_family: "posix",
stdout: "",
stderr: "",
stdout_truncated: false,
stderr_truncated: false,
timed_out: true,
cancelled: false,
effective_timeout_ms: 30_000,
duration_ms: 30_123,
};
},
},
},
});

const { createShellTools } = loader.loadModule("src/lib/tools/shellTools.ts");
const bundle = createShellTools({
workdir: "/repo",
providerId: "deepseek",
runtimePlatform: "macos",
});

const result = await bundle.executeToolCall(createBashCall("make build"));

assert.equal(result.isError, true);
assert.equal(result.details.timed_out, true);
const text = result.content[0].text;
assert.match(text, /timed_out: true/);
assert.match(text, /timeout_ms: 30000/);
assert.match(text, /killed by the shell timeout \(30000ms\)/);
assert.match(text, /it did not crash/);
assert.match(text, /DeepSeek caps Bash at 30000ms/);
assert.match(text, /re-running the identical command unchanged reaches the same limit/);
assert.match(text, /redirect the output to a file/);
});
Loading