fix(agent_loop): M4a死亡螺旋两缺陷根治(2026-09-18 pbls实锤)——①空fallback交付件硬拦截:30轮耗尽零产出时原实现把77字节「未检测到write_file」空说明当交付件submit,且按created_at最新覆盖上轮真实交付件成为QC审查对象;QC面对自相矛盾对象(交付件说未产出/磁盘有git commit)16轮全烧取证无法裁决→no-decision infra回置3连败→fault+pause;正解:零产出=本轮执行失败mark_failed(错误文案带「瞬时/重试」hint走retry分类),绝不制造空壳交付件②_parse_agent_action归一化:LLM输出{"action":"run_shell",...}把工具名写进action字段(M4a QC turn=15实锤),分发只认review_*/tool_call/deliver/ask→落「未识别」纠错分支白烧审核轮次;新增_normalize_action_dict:action=工具名+params/tool字段→归一tool_call,submit/finish→deliver,ask_question→ask,命中TOOL_SCHEMAS宽松兜底;归一失败原样返回保持诚实纠错路径
This commit is contained in:
parent
82257290ee
commit
e6ad1cd8ed
@ -2085,6 +2085,45 @@ def _parse_form_schema(v):
|
||||
return None
|
||||
|
||||
|
||||
_KNOWN_ACTIONS = {'deliver', 'ask', 'tool_call', 'review_approve', 'review_reject',
|
||||
'review_complete', 'review_rollback'}
|
||||
|
||||
|
||||
def _normalize_action_dict(d):
|
||||
"""动作 dict 归一化(2026-09-18 pbls M4a QC 白烧轮次实锤)。
|
||||
|
||||
实测缺陷:LLM 输出 {"action":"run_shell","params":{...}}——把工具名直接写进
|
||||
action 字段而非 "tool_call"。调用方分发只认 _KNOWN_ACTIONS → 落入「未识别输出」
|
||||
分支纠错重试,白烧审核轮次(QC 16 轮预算里 turn=15 实锤)。
|
||||
规则:action 不是已知动作但形似工具调用(带 params/tool 字段,或 action 值
|
||||
出现在工具 schema 里)→ 归一为 tool_call;deliver/ask 同义别名同样归一。
|
||||
归一失败原样返回(保持「未识别→纠错重试」诚实路径,不伪造动作)。
|
||||
"""
|
||||
if not isinstance(d, dict):
|
||||
return d
|
||||
act = str(d.get('action') or '').strip()
|
||||
if act in _KNOWN_ACTIONS:
|
||||
return d
|
||||
if act in ('deliver_result', 'submit', 'finish'):
|
||||
return {**d, 'action': 'deliver'}
|
||||
if act in ('ask_question', 'ask_user'):
|
||||
return {**d, 'action': 'ask', 'question': d.get('question') or d.get('params', {}).get('question', '') if isinstance(d.get('params'), dict) else d.get('question', '')}
|
||||
# action=工具名形态:{"action":"run_shell","params":{...}}
|
||||
if act and ('params' in d or 'tool' in d):
|
||||
return {**d, 'action': 'tool_call', 'tool': d.get('tool') or act,
|
||||
'params': d.get('params') or {}}
|
||||
# 宽松兜底:action 值命中已注册工具 schema 名(capability_tools.TOOL_SCHEMAS)
|
||||
if act:
|
||||
try:
|
||||
from .capability_tools import TOOL_SCHEMAS
|
||||
if act in TOOL_SCHEMAS:
|
||||
return {**d, 'action': 'tool_call', 'tool': act,
|
||||
'params': d.get('params') or {}}
|
||||
except Exception:
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
def _parse_agent_action(raw):
|
||||
raw = (raw or "").strip()
|
||||
if raw.startswith("```"):
|
||||
@ -2133,7 +2172,7 @@ def _parse_agent_action(raw):
|
||||
try:
|
||||
d = json.loads(raw)
|
||||
if isinstance(d, dict) and 'action' in d:
|
||||
return d
|
||||
return _normalize_action_dict(d)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
@ -2145,7 +2184,7 @@ def _parse_agent_action(raw):
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
if isinstance(d, dict) and 'action' in d:
|
||||
return d
|
||||
return _normalize_action_dict(d)
|
||||
# 兼容 {"tool":..,"params":..} 无 action 字段的写法
|
||||
if isinstance(d, dict) and 'tool' in d:
|
||||
return {"action": "tool_call", "tool": d.get("tool", ""), "params": d.get("params") or {}}
|
||||
@ -2167,7 +2206,7 @@ def _parse_agent_action(raw):
|
||||
_idx = _b + 1
|
||||
continue
|
||||
if isinstance(_d, dict) and 'action' in _d:
|
||||
return _d
|
||||
return _normalize_action_dict(_d)
|
||||
if isinstance(_d, dict) and 'tool' in _d:
|
||||
return {"action": "tool_call", "tool": _d.get("tool", ""), "params": _d.get("params") or {}}
|
||||
_idx = _b + max(_end, 1)
|
||||
@ -2853,7 +2892,9 @@ async def _build_fallback_deliverable(space_dir, written_files, role, task_id=''
|
||||
except Exception:
|
||||
pass
|
||||
if not lines:
|
||||
lines.append("(本轮未检测到 write_file 或 git 变更——agent 确实未产出)")
|
||||
# 2026-09-18 pbls M4a:零产出返回 None(调用方 mark_failed),不再返回
|
||||
# 「未检测到 write_file」空说明——空说明会被当交付件送 QC 制造矛盾审查对象。
|
||||
return None
|
||||
return {
|
||||
"result": "\n".join(lines),
|
||||
"summary": "Agent 循环结束未调用 deliver,以下为实际产出检测结果(供 QC 判断是否合格)",
|
||||
@ -3355,6 +3396,21 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
if not deliverable:
|
||||
# 兜底:循环结束未 deliver,检测 write_file/git 实际产出构造真实交付件(而非占位符)
|
||||
deliverable = await _build_fallback_deliverable(space_dir, written_files, role, task_id=task_id)
|
||||
if not deliverable:
|
||||
# 空兜底硬拦截(2026-09-18 pbls M4a 死亡螺旋根治):兜底检测「零产出」时
|
||||
# 原实现仍把 77 字节空说明当交付件 submit → QC 拿到自相矛盾的对象(交付件
|
||||
# 说未产出,磁盘上却有真实 git commit)→ 16 轮全烧在取证调和矛盾、无法
|
||||
# 裁决 → no-decision infra 回置 3 连败 → fault+pause;且空交付件按
|
||||
# created_at 最新覆盖上一轮真实交付件成为 QC 审查对象(_get_deliverable_content
|
||||
# 取最新一条)。正解:零产出=本轮执行失败,mark_failed 走重试/冒泡,
|
||||
# 绝不制造空壳交付件。
|
||||
from .task_capability import mark_failed
|
||||
_err = ("执行轮次耗尽且零产出(瞬时执行失败可重试):agent 未调用 deliver,"
|
||||
"引擎兜底检测无 write_file 写入、无 git 变更。本轮判执行失败"
|
||||
"(不生成空壳交付件送审),自动重试。")
|
||||
await mark_failed(task_id, project_id, who=role, agent_id=agent_id, error=_err)
|
||||
logger.error(f"role_agent_run empty fallback rejected: task={task_id} role={role}")
|
||||
return {"status": "failed", "task_id": task_id, "error": _err}
|
||||
|
||||
# ── 处理产出 ──
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user