Improve write_file fallback handling
This commit is contained in:
+14
-2
@@ -216,7 +216,19 @@ def get_using_your_tools_section(enabled_tool_names: set[str]) -> str:
|
||||
if 'edit_file' in enabled_tool_names:
|
||||
items.append('编辑文件时,优先使用 edit_file,而不是 shell 文本替换。')
|
||||
if 'write_file' in enabled_tool_names:
|
||||
items.append('创建文件时,优先使用 write_file,而不是 heredoc 或 echo 重定向;默认写入当前 session 目录。')
|
||||
items.append(
|
||||
'创建或追加通用文本文件时,优先使用 write_file,而不是 heredoc 或 echo 重定向;默认写入当前 session 目录。'
|
||||
)
|
||||
items.append(
|
||||
'write_file 支持 content、content_lines、content_base64 三种写入方式。多行脚本、Markdown、CSV 或包含较多引号的文本,优先用 content_lines,避免把大段带转义的内容塞进单个 JSON 字符串。'
|
||||
)
|
||||
items.append(
|
||||
'需要分批写入长文件时,使用 write_file 的 append=true 追加稳定文件;不要反复覆盖同一个中间产物。'
|
||||
)
|
||||
if 'python_exec' in enabled_tool_names:
|
||||
items.append(
|
||||
'如果 write_file 因内容过长、引号/换行转义复杂或工具参数生成失败而无法稳定完成写入,下一轮应优先改用 python_exec 写文件:把文本作为 stdin 或 Python 字符串/列表处理,并写入同一个 session 路径。'
|
||||
)
|
||||
if 'glob_search' in enabled_tool_names:
|
||||
items.append('搜索文件时,优先使用 glob_search,而不是 find 或 ls。')
|
||||
if 'grep_search' in enabled_tool_names:
|
||||
@@ -246,7 +258,7 @@ def get_using_your_tools_section(enabled_tool_names: set[str]) -> str:
|
||||
'当没有专用工具,或需要真实 shell 语义、系统命令、进程控制、git 只读检查或用户已确认的依赖安装时,可以使用 bash。'
|
||||
)
|
||||
if 'python_exec' in enabled_tool_names:
|
||||
items.append('当 python_exec 可用时,不要用 bash 执行 python、python3 或 .venv/bin/python。')
|
||||
items.append('当 python_exec 可用时,不要用 bash 执行 python、python3 或 .venv/bin/python;只有 python_exec 和专用文件工具都无法完成时,才把 bash 作为最后兜底。')
|
||||
items.append(
|
||||
'你可以在一次响应中调用多个工具。独立工具调用尽量并行,存在依赖关系的调用保持顺序执行。'
|
||||
)
|
||||
|
||||
@@ -1614,11 +1614,10 @@ class LocalCodingAgent:
|
||||
f'工具:{name};位置:line {exc.lineno} column {exc.colno};'
|
||||
f'原因:{exc.msg};参数片段:{preview!r}。'
|
||||
'请回复“继续”,我会要求模型重新生成合法 JSON 参数;'
|
||||
'对于 product-data 生成数据,不要用 write_file 写大段 draft;'
|
||||
'应每批最多 8 条,直接用 python_exec 的 script_path 模式调用 '
|
||||
'skills/product-data/scripts/normalize_dataset_draft.py,'
|
||||
'通过 stdin 传入小批量 JSON,并把 normalize 结果追加到 '
|
||||
'scratchpad/normalized_records.jsonl。'
|
||||
'如果是 write_file 写多行文本,应改用 content_lines 或 content_base64;'
|
||||
'如果 write_file 仍不稳定,应改用 python_exec 作为通用写文件兜底;'
|
||||
'如果是生成/转换结构化数据,应优先调用对应 skill 脚本或 python_exec,'
|
||||
'不要把大段 JSON/CSV/draft 直接塞进工具参数;bash 仅作为最后兜底。'
|
||||
) from exc
|
||||
if not isinstance(arguments, dict):
|
||||
raise OpenAICompatError(
|
||||
|
||||
@@ -38,14 +38,34 @@ def build_file_tools(handlers: Mapping[str, ToolHandler]) -> list[AgentTool]:
|
||||
),
|
||||
AgentTool(
|
||||
name='write_file',
|
||||
description='Write a complete file inside the workspace. Creates parent directories when needed.',
|
||||
description=(
|
||||
'Write or append a UTF-8 text file inside the workspace. Creates parent directories when needed. '
|
||||
'For multi-line content, prefer content_lines to reduce JSON escaping errors.'
|
||||
),
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'path': {'type': 'string'},
|
||||
'content': {'type': 'string'},
|
||||
'path': {'type': 'string', 'description': 'Relative path. output/, scratchpad/, and input/ route to the current session.'},
|
||||
'content': {'type': 'string', 'description': 'Full text content for short or simple files.'},
|
||||
'content_lines': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
'description': 'Lines to join with "\\n"; recommended for scripts, markdown, CSV, and other multi-line text.',
|
||||
},
|
||||
'required': ['path', 'content'],
|
||||
'content_base64': {
|
||||
'type': 'string',
|
||||
'description': 'Base64-encoded UTF-8 content. Use only when content contains many quotes or complex escaping.',
|
||||
},
|
||||
'append': {
|
||||
'type': 'boolean',
|
||||
'description': 'Append to an existing file instead of replacing it. Defaults to false.',
|
||||
},
|
||||
'newline_at_end': {
|
||||
'type': 'boolean',
|
||||
'description': 'Ensure the written text ends with a newline. Defaults to false.',
|
||||
},
|
||||
},
|
||||
'required': ['path'],
|
||||
},
|
||||
handler=resolve_handler(handlers, 'write_file', 'file'),
|
||||
),
|
||||
@@ -108,4 +128,3 @@ def build_file_tools(handlers: Mapping[str, ToolHandler]) -> list[AgentTool]:
|
||||
handler=resolve_handler(handlers, 'grep_search', 'file'),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
+51
-9
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -1824,23 +1825,34 @@ def _write_file(arguments: dict[str, Any], context: ToolExecutionContext) -> str
|
||||
_ensure_write_allowed(context)
|
||||
target = _resolve_path(_require_string(arguments, 'path'), context)
|
||||
_ensure_not_platform_code_write(target, context)
|
||||
content = arguments.get('content')
|
||||
if not isinstance(content, str):
|
||||
raise ToolExecutionError('content must be a string')
|
||||
content = _coerce_write_file_content(arguments)
|
||||
append = arguments.get('append', False)
|
||||
if not isinstance(append, bool):
|
||||
raise ToolExecutionError('append must be a boolean')
|
||||
newline_at_end = arguments.get('newline_at_end', False)
|
||||
if not isinstance(newline_at_end, bool):
|
||||
raise ToolExecutionError('newline_at_end must be a boolean')
|
||||
if newline_at_end and content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
previous_text: str | None = None
|
||||
previous_sha256: str | None = None
|
||||
if target.exists() and target.is_file():
|
||||
previous_text = target.read_text(encoding='utf-8', errors='replace')
|
||||
previous_sha256 = hashlib.sha256(previous_text.encode('utf-8')).hexdigest()
|
||||
elif target.exists():
|
||||
raise ToolExecutionError(f'Path exists and is not a file: {target}')
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content, encoding='utf-8')
|
||||
final_text = (previous_text or '') + content if append else content
|
||||
target.write_text(final_text, encoding='utf-8')
|
||||
rel = _display_path(target, context)
|
||||
new_sha256 = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
new_sha256 = hashlib.sha256(final_text.encode('utf-8')).hexdigest()
|
||||
action = 'append_file' if append else 'write_file'
|
||||
return (
|
||||
f'wrote {rel} ({len(content)} chars)',
|
||||
f'{"appended" if append else "wrote"} {rel} ({len(content)} chars)',
|
||||
{
|
||||
'action': 'write_file',
|
||||
'action': action,
|
||||
'path': rel,
|
||||
'append': append,
|
||||
'before_exists': previous_text is not None,
|
||||
'before_sha256': previous_sha256,
|
||||
'before_size': len(previous_text) if previous_text is not None else 0,
|
||||
@@ -1850,13 +1862,43 @@ def _write_file(arguments: dict[str, Any], context: ToolExecutionContext) -> str
|
||||
else None
|
||||
),
|
||||
'after_sha256': new_sha256,
|
||||
'after_size': len(content),
|
||||
'after_preview': _snapshot_text(content),
|
||||
'after_size': len(final_text),
|
||||
'after_preview': _snapshot_text(final_text),
|
||||
'content_length': len(content),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _coerce_write_file_content(arguments: dict[str, Any]) -> str:
|
||||
provided = [
|
||||
key
|
||||
for key in ('content', 'content_lines', 'content_base64')
|
||||
if key in arguments and arguments[key] is not None
|
||||
]
|
||||
if len(provided) != 1:
|
||||
raise ToolExecutionError(
|
||||
'Provide exactly one of content, content_lines, or content_base64'
|
||||
)
|
||||
source = provided[0]
|
||||
if source == 'content':
|
||||
content = arguments['content']
|
||||
if not isinstance(content, str):
|
||||
raise ToolExecutionError('content must be a string')
|
||||
return content
|
||||
if source == 'content_lines':
|
||||
content_lines = arguments['content_lines']
|
||||
if not isinstance(content_lines, list) or not all(isinstance(line, str) for line in content_lines):
|
||||
raise ToolExecutionError('content_lines must be an array of strings')
|
||||
return '\n'.join(content_lines)
|
||||
encoded = arguments['content_base64']
|
||||
if not isinstance(encoded, str):
|
||||
raise ToolExecutionError('content_base64 must be a string')
|
||||
try:
|
||||
return base64.b64decode(encoded, validate=True).decode('utf-8')
|
||||
except (ValueError, UnicodeDecodeError) as exc:
|
||||
raise ToolExecutionError('content_base64 must be valid base64-encoded UTF-8 text') from exc
|
||||
|
||||
|
||||
def _edit_file(arguments: dict[str, Any], context: ToolExecutionContext) -> str:
|
||||
_ensure_write_allowed(context)
|
||||
target = _resolve_path(_require_string(arguments, 'path'), context, allow_missing=False)
|
||||
|
||||
Reference in New Issue
Block a user