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
|
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):
|
def _parse_agent_action(raw):
|
||||||
raw = (raw or "").strip()
|
raw = (raw or "").strip()
|
||||||
if raw.startswith("```"):
|
if raw.startswith("```"):
|
||||||
@ -2133,7 +2172,7 @@ def _parse_agent_action(raw):
|
|||||||
try:
|
try:
|
||||||
d = json.loads(raw)
|
d = json.loads(raw)
|
||||||
if isinstance(d, dict) and 'action' in d:
|
if isinstance(d, dict) and 'action' in d:
|
||||||
return d
|
return _normalize_action_dict(d)
|
||||||
except (json.JSONDecodeError, ValueError):
|
except (json.JSONDecodeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -2145,7 +2184,7 @@ def _parse_agent_action(raw):
|
|||||||
except (json.JSONDecodeError, ValueError):
|
except (json.JSONDecodeError, ValueError):
|
||||||
continue
|
continue
|
||||||
if isinstance(d, dict) and 'action' in d:
|
if isinstance(d, dict) and 'action' in d:
|
||||||
return d
|
return _normalize_action_dict(d)
|
||||||
# 兼容 {"tool":..,"params":..} 无 action 字段的写法
|
# 兼容 {"tool":..,"params":..} 无 action 字段的写法
|
||||||
if isinstance(d, dict) and 'tool' in d:
|
if isinstance(d, dict) and 'tool' in d:
|
||||||
return {"action": "tool_call", "tool": d.get("tool", ""), "params": d.get("params") or {}}
|
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
|
_idx = _b + 1
|
||||||
continue
|
continue
|
||||||
if isinstance(_d, dict) and 'action' in _d:
|
if isinstance(_d, dict) and 'action' in _d:
|
||||||
return _d
|
return _normalize_action_dict(_d)
|
||||||
if isinstance(_d, dict) and 'tool' in _d:
|
if isinstance(_d, dict) and 'tool' in _d:
|
||||||
return {"action": "tool_call", "tool": _d.get("tool", ""), "params": _d.get("params") or {}}
|
return {"action": "tool_call", "tool": _d.get("tool", ""), "params": _d.get("params") or {}}
|
||||||
_idx = _b + max(_end, 1)
|
_idx = _b + max(_end, 1)
|
||||||
@ -2853,7 +2892,9 @@ async def _build_fallback_deliverable(space_dir, written_files, role, task_id=''
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not lines:
|
if not lines:
|
||||||
lines.append("(本轮未检测到 write_file 或 git 变更——agent 确实未产出)")
|
# 2026-09-18 pbls M4a:零产出返回 None(调用方 mark_failed),不再返回
|
||||||
|
# 「未检测到 write_file」空说明——空说明会被当交付件送 QC 制造矛盾审查对象。
|
||||||
|
return None
|
||||||
return {
|
return {
|
||||||
"result": "\n".join(lines),
|
"result": "\n".join(lines),
|
||||||
"summary": "Agent 循环结束未调用 deliver,以下为实际产出检测结果(供 QC 判断是否合格)",
|
"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:
|
if not deliverable:
|
||||||
# 兜底:循环结束未 deliver,检测 write_file/git 实际产出构造真实交付件(而非占位符)
|
# 兜底:循环结束未 deliver,检测 write_file/git 实际产出构造真实交付件(而非占位符)
|
||||||
deliverable = await _build_fallback_deliverable(space_dir, written_files, role, task_id=task_id)
|
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
|
from appPublic.uniqueID import getID
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user