fix(run_python): 超时返回超时前的部分输出 + 无缓冲(-u),模型据此续跑不整批重来(bump 0.58.36)
失败面板 #8(command timed out 19 次/7 task,累计最多)。定性(查 19 条超时的原始 tool_call 代码):不是死循环/chromium 老坑,全是真慢的批量 I/O——① 论文检索 skills.research.paper.search() 循环搜多期刊/关键词(最多,每次打 OpenAlex 累计爆超时) ② pdfplumber 多 PDF extract_text ③ 大文件下载 ④ pandas 大 Excel。超时机制正常,真正 浪费在 _run_subprocess 超时分支把已捕获的部分输出全丢了(kill 后 communicate() 已续读 超时前 stdout,却只回一句 command timed out → 模型看不到"搜到第6个"、整批重搜)。 修: 1. tools/base.py::format_timeout_result 把超时前 stdout/stderr 一并返回 + 续跑提示 (据已完成部分续跑/每项落盘/大操作用 background=true/单次别塞太多),docker (_run_subprocess)+ host(run_python TimeoutExpired.stdout/stderr)两执行器共用。 2. -u 无缓冲:Python 管道下 stdout 块缓冲,不 flush 的 print 超时被 kill 时缓冲区就丢, 故 run_python 的 python 调用全加 -u(docker 前台 inline/script_path + host 前台 + 两处后台 procs.launch_host),让部分输出真正落管道被捕获(不碰 shell)。 提示指路(background/续跑)与语法预检不同、恰当:超时是"你这段太慢"的确定信号, 建议对任何超时都可靠。不做:不抬默认 timeout(120s 合理);不单独改 prompt。 测试 tests/test_timeout_partial.py(format 单测 + host 端到端:print 两项后 sleep timeout=1 → 结果带两项 + 续跑提示)+ test_executor_docker argv 断言更新为 python -u, 全 34 相邻测试绿。additive(超时结果加信息不破坏 schema),不碰对外契约。至此失败面板 top 全部处置(② edit streak/③ 畸形/① SyntaxError 预检+渲染器/#8 超时)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5cf92ec71a
commit
1581b69bfa
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-07
|
||||
|
||||
- **07-17 / 0.58.36**:**run_python 超时返回超时前的部分输出 + 无缓冲(-u),让模型据此续跑不整批重来**(失败面板 #8:`command timed out` 19 次 / 7 task / 6 用户,累计最多)。**定性**(查 19 条超时的原始 tool_call 代码):不是死循环 / chromium 老坑,全是**真慢的批量 I/O**——① 论文检索 `skills.research.paper.search()` 循环搜多个期刊 / 关键词(最多,~11 条,每次打 OpenAlex 外部 API 累计爆超时)② pdfplumber 多 PDF `extract_text` ③ 大文件下载(ffmpeg tar.xz / MDPI PDF)④ pandas 大 Excel。超时机制本身正常,真正浪费在 **`_run_subprocess` 超时分支把已捕获的部分输出全丢了**(kill 后 `communicate()` 已续读超时前 stdout,却只回一句 `[Error] command timed out` → 模型看不到「搜到第 6 个」、整批重搜,同 task 反复重跑)。**修**:① `tools/base.py::format_timeout_result` 把超时前的 stdout/stderr 一并返回 + 续跑提示(据已完成部分续跑 / 每项落盘 / 大操作用 `background=true` / 单次别塞太多),docker(`_run_subprocess`)+ host(`run_python.py` 的 `TimeoutExpired.stdout/stderr`)两执行器共用;② **`-u` 无缓冲**——Python 管道下 stdout 块缓冲,不 flush 的 `print` 超时被 kill 时缓冲区就丢了,故 run_python 的 python 调用全加 `-u`(docker 前台 inline / script_path + host 前台 + 两处后台 `procs.launch_host`),让部分输出真正落管道被捕获(不碰 shell)。这里提示指路(background / 续跑)与语法预检不同、**恰当**:超时就是「你这段太慢」的确定信号,建议对任何超时都可靠,不像语法→渲染器那样分不清场景。**不做**:不抬默认 timeout(120s 合理,抬高只延后同问题 + 多占 loop);不单独改 prompt(提示随超时结果反应式投放更精准)。测试 `tests/test_timeout_partial.py`(format 单测 + host 端到端:print 两项后 sleep,timeout=1 → 结果带两项 + 续跑提示)+ `test_executor_docker` argv 断言更新为 `python -u`,全 34 相邻测试绿。additive(超时结果加信息,不破坏 schema),不碰对外 API/DB 契约。至此失败面板 top 全部处置(② edit streak / ③ 畸形 / ① SyntaxError 预检+渲染器 / #8 超时);char-0 畸形由既有 salvage 覆盖。相关 memory:高轮数烧 token 三根因 / 沙箱 chromium 案(本次确认非 chromium)。
|
||||
- **07-17 / 0.58.35**:**通用报告走平台渲染器(`report` profile)+ 基座引导——从根上少让模型手撸 python-docx**(接 0.58.34:语法预检是止血,这条是根治那一头)。**关键发现**:平台**早有**成熟 md→docx(+pdf)渲染器 `rendering/`(bind-mount `/sandbox/rendering:ro`,任何 run_python/shell 可达,与 skill 无关),标题/正文/列表/表格/图片图题/mermaid/内联/化学式下标/目录/house 字体(宋体小四·黑体·Times·1.5 行距)全有,paper/proposal/brief 三 profile 在用。烧 token 的验收报告没走它、在裸手撸 python-docx,**唯一原因是没 skill/引导把"随手写 Word 报告"指向它**(只有 proposal/paper 知道)。故**不造轮子**,只两件小事:① `rendering/docx_manuscript.py` 加 `report` profile(复用全套渲染,差异=报告不是申报书:目录默认无[`--toc` 才带]、章节不强制分页、通用页边距 2.5/2.5、列表含"第X章/节"不含"条"),`render.py --profile` 加 `report` 选项 + 路由;② `core/agent_builder.py` 基座 prompt 加软引导「出 Word 报告→正文写成 `<task_dir>/sections/*.md`(纯文本零转义)调 `render.py --profile report`,别手撸 python-docx 内联中文」。**取舍红线(与用户对齐)**:渲染器是**首选加法不是替代**——绝不硬拦 run_python 生成文档,确需精细定制版式/改写已有 docx/处理 xlsx 等渲染器覆盖不了的照样自由写代码;软引导让位于更具体的 skill render 指引(proposal/paper)。**刻意不做**:(c) 把渲染器指路塞进语法预检 hint——会把只该管语法的检查overload成它做不可靠的工具选择判断(分不清"报告正文该走渲染器"vs"定制 python-docx 就该手撸"),误导合法手撸,故预检保持只诊断语法。三层叠加:渲染器(根治)> 预检(0.58.34 安全网)> RepeatGuard err-streak(0.58.33 兜底),run_python 能力不削。测试 `tests/test_rendering.py`(四 profile 端到端 + report 目录可选)+ CLI e2e(`render.py --profile report` 出 37KB docx)全绿;py_compile 校 agent_builder/render/manuscript 无语法错。DESIGN §8.6 记该 profile + 并存红线。additive,不碰对外 API/DB 契约。相关 memory:skill 禁令不带配方 / 高轮数烧 token 三根因。
|
||||
- **07-17 / 0.58.34**:**run_python 语法预检:宿主 compile() 拦下中文正文硬拼进源码的语法坏码**(失败面板最大头,20 条真实样本实证:模型手写 python-docx 生成 docx/报告时把中文正文内联进 `T("中文…")`,引号/标点崩坏 → SyntaxError → 改 → 再崩,`gen_acceptance_report_v3.py` 一分钟内连撞 6 次)。三类:**A 引号提前闭合**(最多,中文串里用 ASCII 引号 `"` 把外层字符串提前闭合;Python 却报 `Perhaps you forgot a comma?`/`unterminated string`,**主动误导**模型照着加逗号越修越错)、**B 全角标点漏进代码位**(`center=True、`/`采购(`,`invalid character 'X'(U+XXXX)`)、**C 括号/引号结构崩**。**根因判断**:症结不是"缺写长文的工具"(write 早能写长文进文件),而是**docx/报告没有渲染管线**——documents skill 只做文献检索、不管 docx 生成,模型裸手撸 python-docx 内联中文;而 ppt skill 有 SVG→pptx 管线(正文进 SVG 文本节点),所以 ppt 崩得少。**本轮先落机检止血**(对标 [[feedback_skill_ban_no_recipe]] "硬约束靠平台机检"):新增独立模块 `core/pysyntax.py`(不 import litellm,`tests/test_pysyntax.py` 本机可跑,11 用例覆盖 A/B/C + 合法中文串/中文引号不误伤)`precheck_python(code)`——host 上 `compile()`(host 3.12 ≡ sandbox python:3.12-slim,零版本偏差;只解析不执行、与依赖无关),命中 SyntaxError 返**锐化中文诊断**(出错行+光标+按错误类型定向:invalid-character→全角改半角、forgot-comma/unterminated+含中文→ASCII 引号提前闭合的三改法、含中文统一追加"正文别拼进 .py,write 进 .md/.txt 再 read"根治提示)。接进 **run_python 所有派发路径**(inline/script_path/background × docker+host 两执行器,`_prepare_script` 内嵌覆盖 script_path 全路径 + inline 分支各加一处):命中即 `ToolResult(hint, exit_code=2)` 早返、**跳过 docker 往返**。**零误伤**:只拦本就跑不了的码(compile 不过=运行必失败),通过则原样放行、行为不变;既有 30 个 executor/run_python 测试(合法码 fixture)全绿印证。**白捡协同**:预检把 run_python 语法失败从 `[stderr]…[exit 1]` 改成 `[Error]` 前缀+固定首行,`loop._RepeatGuard` 的 err-streak(0.58.33 ②)得以兜住"反复交语法坏码"的循环(旧 `[exit 1]` 形态它按非错误放行、抓不到)——① 诊断准(修得快)+ ② 撞墙拦(停得住)合围。**下一步(未做,单列选型)**:docx 渲染管线(正文 write 进纯文本文件 → 渲染工具 read 生成 docx,正文永不进 Python 源码,**也不能走"大正文当工具入参"**——那会把长中文塞进 tool_call arguments,触发 0.58.33 修的 wire 乱序畸形);沙箱现有 python-docx+markitdown(docx→md 单向)、无 pandoc/libreoffice,自建渲染器 vs 装 pandoc 待拍板。additive 内部机制,不碰对外 API/DB 契约。相关 memory:skill 禁令不带配方 / deepseek 畸形 salvage 案。
|
||||
- **07-17 / 0.58.33**:**堵工具失败聚集里两条反复烧 token 的裂缝**(扫近 14 天失败面板 top13 定位,`scripts/standalone` 复现口径)。**裂缝①「必填 key 被吞」畸形(面板 #3 `edit 缺少必填参数 ['path']`,14 次 / 13 task / 9 用户,最广)**:定层查一条原始 tool_call 坐实是**流式 wire 乱序**——某键的值碎片被瞬移拼进相邻字符串(实证:path 的 `.../gen_final_report_v2.py` 被吞进 `old_str` 尾部,独立的 `"path"` 键随之消失),跨 provider(`call_00_` 网关档 + `toolu_` Anthropic 系),非模型漏参。这类与既有 char-0 前缀畸形的关键区别是 **JSON parse 成功** → 既不命中 `_malformed_tool_calls`(只抓 parse 失败)、salvage 也救不了(parse-to-end/key 白名单对合法但错位无能),一路漏到 executor 才在语义层报错、回 `[Error]` 喂回模型 → 再拼再乱序反复烧。**修**:`_toolcalls_partial_args`(解析为非空 dict 且 `0<len(missing_required)<len(required)`,即至少一个必填在场、至少一个缺失=错位吞键特征;空 `{}`/必填全缺不算,仍交 executor+RepeatGuard)接进 `_stream_llm` 的 attempt 循环,和畸形/空响应同类:丢弃本轮走**非流式重试**(服务端一次拼好绕开流式 delta 乱序),耗尽仍缺才落回 executor 返「缺必填参数」(多为真漏参,不再空转);`_log_partial_args` 落 `usage_events`(kind=`tool_malformed`,error 签名固定 `missing required keys [...]`,和 char-0 型区分)——这类此前伪装成普通 messages `[Error]`、面板完全看不见,现纳入可观测。**裂缝②「换参数撞同一堵墙」(面板 #2 `edit old_str not found` 单 task 9 次 / 24h 7 次)**:模型每次微调 old_str 重试,精确 args 指纹每次不同 → `_RepeatGuard` 的 arg 判据不累计,只能等 `_STALL_LIMIT` 慢慢兜。**修**:`_RepeatGuard` 补第二道判据——按工具的**连续同类错误 streak**(`_norm_err` 抹平路径/数字后签名相同、跨 ≥2 个不同 args),SOFT 注入定向提示(先 read/grep 看确切内容再动手)、HARD 拦截一次并重置到 SOFT(非永久封死,换路后重试留活口);任一非错误结果立即清零该工具 streak。测试 `tests/test_loop_repeat_guard.py` +12(err-streak 5 + partial-args 7),全 23 相邻测试(salvage/toolfail_malformed)绿。**本机验证坑**:`core/loop.py` 顶层 `import litellm` 本机导入死卡(GitHub cost map),纯逻辑测试用 `sys.modules` 注入 litellm+子模块 stub 绕开跑真代码;失败面板扫描脚本因 import 链同样卡,改直连 psycopg 复刻 `scan_tool_failures` 口径查(结果与走 `core` 的交叉验证一致)。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测 + 生产灰度。additive 内部机制,不碰对外 API/DB 契约。剩 ①run_python 中文全角标点/未闭合字符串致 SyntaxError(面板最大头,产物质量,待单独立项)、③超时 19 次未动。相关 memory:deepseek 畸形 salvage 案 / 高轮数烧 token 三根因。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.58.35"
|
||||
__version__ = "0.58.36"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ import json
|
|||
# canceller 侧线程 poll cancel_check 的间隔;单测可 patch 此常量加速
|
||||
_CANCEL_POLL_INTERVAL_S = 0.2
|
||||
|
||||
from tools.base import format_timeout_result
|
||||
|
||||
from .executor import ExecCtx, Executor, ToolResult
|
||||
from .executor_host import HostExecutor
|
||||
from .pysyntax import precheck_python
|
||||
|
|
@ -213,7 +215,7 @@ class DockerExecutor(Executor):
|
|||
argv = self._docker_exec_argv(
|
||||
container,
|
||||
extra_env=_sandbox_env({"PYTHONIOENCODING": "utf-8"}),
|
||||
) + ["python", container_script]
|
||||
) + ["python", "-u", container_script]
|
||||
result = self._run_subprocess(argv, timeout=timeout, ctx=ctx)
|
||||
self.pool.mark_active(self.user_id)
|
||||
return self._compact_shell_like_result(result)
|
||||
|
|
@ -242,7 +244,7 @@ class DockerExecutor(Executor):
|
|||
argv = self._docker_exec_argv(
|
||||
container,
|
||||
extra_env=_sandbox_env({"PYTHONIOENCODING": "utf-8"}),
|
||||
) + ["python", container_script] # 删 setsid 同上(_exec_shell 注释)
|
||||
) + ["python", "-u", container_script] # 删 setsid 同上(_exec_shell 注释)
|
||||
result = self._run_subprocess(argv, timeout=timeout, ctx=ctx)
|
||||
self.pool.mark_active(self.user_id)
|
||||
return self._compact_shell_like_result(result)
|
||||
|
|
@ -304,14 +306,14 @@ class DockerExecutor(Executor):
|
|||
container_script, err = self._prepare_script(script_path, code)
|
||||
if err is not None:
|
||||
return ToolResult(content=err, exit_code=2)
|
||||
inner = f"python {container_script}"
|
||||
inner = f"python -u {container_script}"
|
||||
display, kind = f"python {script_path.strip()}", "python"
|
||||
elif isinstance(code, str) and code.strip():
|
||||
hint = precheck_python(code)
|
||||
if hint:
|
||||
return ToolResult(content=hint, exit_code=2)
|
||||
(d / "script.py").write_text(code, encoding="utf-8", newline="\n")
|
||||
inner = f"python {cdir}/script.py"
|
||||
inner = f"python -u {cdir}/script.py"
|
||||
display, kind = f"python <inline {len(code)} chars>", "python"
|
||||
else:
|
||||
return ToolResult(
|
||||
|
|
@ -572,8 +574,9 @@ class DockerExecutor(Executor):
|
|||
content="[Error] command cancelled by user", exit_code=130
|
||||
)
|
||||
if timeout_hit:
|
||||
# kill 后 communicate() 已续读超时前的部分输出(见上),别丢 —— 带上让模型续跑
|
||||
return ToolResult(
|
||||
content=f"[Error] command timed out after {timeout}s",
|
||||
content=format_timeout_result(stdout, stderr, timeout),
|
||||
exit_code=124,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ def launch_host(
|
|||
if inline_code is not None:
|
||||
script = d / "script.py"
|
||||
script.write_text(inline_code, encoding="utf-8")
|
||||
argv = [sys.executable, str(script)]
|
||||
argv = [sys.executable, "-u", str(script)] # 无缓冲:进度实时落 output.log,超时/check 可见
|
||||
|
||||
meta: Dict[str, Any] = {
|
||||
"proc_id": proc_id,
|
||||
|
|
|
|||
|
|
@ -339,9 +339,9 @@ class TestRunPython(unittest.TestCase):
|
|||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
argv = captured_argv[0]
|
||||
# 末尾形态:python /workspace/.zcbot_tmp/<task_id>/<rand>.py
|
||||
# (2026-05-29 删 setsid,见 _exec_python 注释)
|
||||
self.assertEqual(argv[-2], "python")
|
||||
# 末尾形态:python -u /workspace/.zcbot_tmp/<task_id>/<rand>.py
|
||||
# (2026-05-29 删 setsid;-u 无缓冲让超时前部分输出可捕获,见 format_timeout_result)
|
||||
self.assertEqual(argv[-3:-1], ["python", "-u"])
|
||||
self.assertTrue(argv[-1].startswith(f"/workspace/{TMP_SUBDIR}/{ctx.task_id}/"))
|
||||
self.assertTrue(argv[-1].endswith(".py"))
|
||||
# PYTHONIOENCODING / PYTHONPATH 注入
|
||||
|
|
@ -382,7 +382,7 @@ class TestRunPython(unittest.TestCase):
|
|||
|
||||
self.assertIn("[stdout]\nok", result.content)
|
||||
argv = captured_argv[0]
|
||||
self.assertEqual(argv[-2], "python")
|
||||
self.assertEqual(argv[-3:-1], ["python", "-u"])
|
||||
self.assertEqual(argv[-1], "/workspace/demo/scripts/job.py")
|
||||
tmp_subroot = executor.user_root / TMP_SUBDIR / str(ctx.task_id)
|
||||
self.assertFalse(tmp_subroot.exists())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
"""run_python/shell 超时返回部分输出(失败面板 #8:批量 I/O 超时丢进度反复重跑)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from tools.base import format_timeout_result
|
||||
from tools.run_python import RunPythonTool
|
||||
|
||||
|
||||
class TestFormatTimeoutResult(unittest.TestCase):
|
||||
def test_includes_partial_stdout_and_hint(self):
|
||||
out = format_timeout_result("[OK] journal 1\n[OK] journal 2\n", "", 120)
|
||||
self.assertIn("[stdout]", out)
|
||||
self.assertIn("[OK] journal 1", out)
|
||||
self.assertIn("[OK] journal 2", out)
|
||||
self.assertIn("timed out after 120s", out)
|
||||
self.assertIn("续跑", out) # 续跑提示
|
||||
self.assertIn("background=true", out) # 后台指路
|
||||
|
||||
def test_includes_partial_stderr(self):
|
||||
out = format_timeout_result("", "warn: slow\n", 60)
|
||||
self.assertIn("[stderr]", out)
|
||||
self.assertIn("warn: slow", out)
|
||||
self.assertIn("timed out after 60s", out)
|
||||
|
||||
def test_empty_output_still_reports_timeout(self):
|
||||
out = format_timeout_result("", "", 30)
|
||||
self.assertNotIn("[stdout]", out)
|
||||
self.assertNotIn("[stderr]", out)
|
||||
self.assertIn("timed out after 30s", out)
|
||||
|
||||
|
||||
class TestHostTimeoutPartial(unittest.TestCase):
|
||||
def test_partial_progress_returned_on_timeout(self):
|
||||
"""前台跑到一半超时:-u 无缓冲让进度落盘,超时结果带上 → 模型可据此续跑。"""
|
||||
code = (
|
||||
"print('[OK] item 1')\n"
|
||||
"print('[OK] item 2')\n"
|
||||
"import time\n"
|
||||
"time.sleep(30)\n"
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = RunPythonTool(base_dir=tmp).execute(code=code, timeout=1)
|
||||
self.assertIn("timed out after 1s", out)
|
||||
self.assertIn("[OK] item 1", out) # -u 无缓冲,部分输出被捕获
|
||||
self.assertIn("[OK] item 2", out)
|
||||
self.assertIn("续跑", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -36,6 +36,32 @@ def compact_tool_output(
|
|||
)
|
||||
|
||||
|
||||
_TIMEOUT_HINT = (
|
||||
"若是批量循环(检索 / PDF 抽取 / 下载等):① 据上面已完成的部分**续跑剩下的**,别整批重跑;"
|
||||
"② 每完成一项就落盘 / 打印进度,超时也不丢;③ 大批量 / 大下载改用 background=true 后台跑,"
|
||||
"再用 check_process 取结果;④ 单次别塞太多项。"
|
||||
)
|
||||
|
||||
|
||||
def format_timeout_result(stdout: str, stderr: str, timeout_s: int) -> str:
|
||||
"""run_python / shell 超时结果:带上超时前已捕获的部分输出 + 续跑提示。
|
||||
|
||||
进程被 kill 前跑出的 stdout/stderr 本已捕获(subprocess 标准行为:TimeoutExpired
|
||||
带 .stdout/.stderr、docker 路径 kill 后 communicate() 续读),旧实现直接丢弃只回一句
|
||||
超时 → 模型看不到「跑到哪了」、整批重来(工具失败面板 #8:批量检索 / PDF 抽取 / 下载
|
||||
超时反复重跑,累计最多)。这里把部分输出一并返回,让模型据此续跑未完成的。"""
|
||||
parts = []
|
||||
if stdout and stdout.strip():
|
||||
parts.append(f"[stdout]\n{stdout.rstrip()}")
|
||||
if stderr and stderr.strip():
|
||||
parts.append(f"[stderr]\n{stderr.rstrip()}")
|
||||
parts.append(
|
||||
f"[Error] command timed out after {timeout_s}s(进程已被杀,上方为超时前的部分输出)。"
|
||||
)
|
||||
parts.append(_TIMEOUT_HINT)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
class Tool(ABC):
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from typing import Optional
|
|||
from core import procs
|
||||
from core.pysyntax import precheck_python
|
||||
|
||||
from .base import Tool, compact_tool_output
|
||||
from .base import Tool, compact_tool_output, format_timeout_result
|
||||
|
||||
_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")
|
||||
# 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key
|
||||
|
|
@ -140,7 +140,7 @@ class RunPythonTool(Tool):
|
|||
anchor, self.task_id,
|
||||
kind="python",
|
||||
command_display=f"python {self._display(script)}",
|
||||
argv=[sys.executable, str(script)],
|
||||
argv=[sys.executable, "-u", str(script)],
|
||||
cwd=self.base_dir, timeout_s=timeout_s, env=self._filtered_env(),
|
||||
)
|
||||
shown = self._display(script)
|
||||
|
|
@ -195,7 +195,7 @@ class RunPythonTool(Tool):
|
|||
try:
|
||||
env = self._filtered_env()
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script)],
|
||||
[sys.executable, "-u", str(script)],
|
||||
cwd=str(self.base_dir),
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
|
|
@ -204,10 +204,13 @@ class RunPythonTool(Tool):
|
|||
errors="replace",
|
||||
env=env,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return (
|
||||
f"[Error] python script timed out after {timeout}s. "
|
||||
f"若任务本身需要长时间运行,用 background=true 重新发起(后台执行,check_process 查进度)。"
|
||||
except subprocess.TimeoutExpired as e:
|
||||
# TimeoutExpired 带 .stdout/.stderr(capture_output 已捕获超时前的输出);
|
||||
# 带上让模型据此续跑,别整批重来(失败面板 #8)。bytes→str 兜底(text=True 应已是 str)
|
||||
def _s(v):
|
||||
return v.decode("utf-8", "replace") if isinstance(v, bytes) else (v or "")
|
||||
return compact_tool_output(
|
||||
format_timeout_result(_s(e.stdout), _s(e.stderr), timeout)
|
||||
)
|
||||
finally:
|
||||
if cleanup_script:
|
||||
|
|
|
|||
Loading…
Reference in New Issue