refactor(tools): 输出处理原语移出 base.py——base 只留 Tool 基类与路径边界

compact_tool_output / format_timeout_result 是「工具输出处理」关注点,与
Tool ABC / FileOutOfBounds 路径安全混装在 base.py(审查小项)。析出
tools/output.py,8 处引用(5 工具 + executor_docker + 2 测试)同步改。
318 测试全过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-23 13:15:04 +08:00
parent 8038fb7491
commit 5a5ebfbe24
10 changed files with 62 additions and 55 deletions

View File

@ -47,13 +47,13 @@ import json
# canceller 侧线程 poll cancel_check 的间隔;单测可 patch 此常量加速 # canceller 侧线程 poll cancel_check 的间隔;单测可 patch 此常量加速
_CANCEL_POLL_INTERVAL_S = 0.2 _CANCEL_POLL_INTERVAL_S = 0.2
from tools.base import format_timeout_result from tools.output import format_timeout_result
from .executor import ExecCtx, Executor, ToolResult from .executor import ExecCtx, Executor, ToolResult
from .executor_host import HostExecutor from .executor_host import HostExecutor
from .pysyntax import precheck_python from .pysyntax import precheck_python
from .sandbox import SandboxPool from .sandbox import SandboxPool
from tools.base import compact_tool_output from tools.output import compact_tool_output
# write/edit 走配额 gate;read/glob/grep 不消耗磁盘,放行 # write/edit 走配额 gate;read/glob/grep 不消耗磁盘,放行

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import tempfile import tempfile
import unittest import unittest
from tools.base import format_timeout_result from tools.output import format_timeout_result
from tools.run_python import RunPythonTool from tools.run_python import RunPythonTool

View File

@ -1,7 +1,7 @@
import unittest import unittest
import tempfile import tempfile
from tools.base import compact_tool_output from tools.output import compact_tool_output
from tools.run_python import RunPythonTool from tools.run_python import RunPythonTool

View File

@ -16,52 +16,6 @@ class FileOutOfBounds(Exception):
"""agent 给的文件路径解析后落在 user_root 之外(防 `../` 读到别人/系统文件)。""" """agent 给的文件路径解析后落在 user_root 之外(防 `../` 读到别人/系统文件)。"""
def compact_tool_output(
text: str,
*,
max_chars: int = 8_000,
head_chars: int = 4_000,
tail_chars: int = 2_000,
) -> str:
"""压缩长工具输出,保留头尾和截断说明。"""
if len(text) <= max_chars:
return text
head_chars = max(0, min(head_chars, max_chars))
tail_chars = max(0, min(tail_chars, max_chars - head_chars))
removed = len(text) - head_chars - tail_chars
return (
text[:head_chars]
+ f"\n[... truncated, {removed} chars omitted ...]\n"
+ (text[-tail_chars:] if tail_chars else "")
)
_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/.stderrdocker 路径 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): class Tool(ABC):
name: str = "" name: str = ""
description: str = "" description: str = ""

View File

@ -12,7 +12,8 @@ from typing import Optional
from core import procs from core import procs
from .base import Tool, compact_tool_output from .base import Tool
from .output import compact_tool_output
class CheckProcessTool(Tool): class CheckProcessTool(Tool):

View File

@ -16,7 +16,8 @@ from uuid import UUID
from core.ark_client import ArkConfig from core.ark_client import ArkConfig
from core.storage.usage import record_vision_usage from core.storage.usage import record_vision_usage
from .base import Tool, compact_tool_output from .base import Tool
from .output import compact_tool_output
from .image_ref import load_image_as_data_url from .image_ref import load_image_as_data_url
from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe

48
tools/output.py Normal file
View File

@ -0,0 +1,48 @@
"""工具输出处理原语(从 tools/base.py 析出,2026-07-23——base 只留 Tool 基类与路径边界)。"""
from __future__ import annotations
def compact_tool_output(
text: str,
*,
max_chars: int = 8_000,
head_chars: int = 4_000,
tail_chars: int = 2_000,
) -> str:
"""压缩长工具输出,保留头尾和截断说明。"""
if len(text) <= max_chars:
return text
head_chars = max(0, min(head_chars, max_chars))
tail_chars = max(0, min(tail_chars, max_chars - head_chars))
removed = len(text) - head_chars - tail_chars
return (
text[:head_chars]
+ f"\n[... truncated, {removed} chars omitted ...]\n"
+ (text[-tail_chars:] if tail_chars else "")
)
_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/.stderrdocker 路径 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)

View File

@ -18,7 +18,8 @@ from uuid import UUID
from core.ark_client import ArkConfig from core.ark_client import ArkConfig
from core.storage.usage import record_vision_usage from core.storage.usage import record_vision_usage
from .base import Tool, compact_tool_output from .base import Tool
from .output import compact_tool_output
from .image_ref import _CONTAINER_ROOT, load_pdf_as_data_url, resolve_in_root from .image_ref import _CONTAINER_ROOT, load_pdf_as_data_url, resolve_in_root
from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe

View File

@ -18,7 +18,8 @@ from typing import Optional
from core import procs from core import procs
from core.pysyntax import precheck_python from core.pysyntax import precheck_python
from .base import Tool, compact_tool_output, format_timeout_result from .base import Tool
from .output import compact_tool_output, format_timeout_result
_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY") _SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")
# 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key # 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key

View File

@ -11,7 +11,8 @@ from typing import Optional
from core import procs from core import procs
from .base import Tool, compact_tool_output from .base import Tool
from .output import compact_tool_output
class ShellTool(Tool): class ShellTool(Tool):