fix(agent): 交付件类型守卫+本体文件清单——deliverable_type照抄示例值零校验(pbls需求交付被标design_doc),RoleSpec.deliverable_types声明合法清单,deliver拦截校验(native+文本双路径)非法拒绝回填清单,唯一合法类型缺省自动填充;交付件快照文件名清洗(getID '-'开头难用);新增files_json本体文件清单列(write_file实写+deliver files),PM/QC审核输入随附清单+强制read_file本体核对(只看6000字摘要索引误判产出规模)

This commit is contained in:
ymq 2026-09-15 00:11:56 +08:00
parent 7f3ce26295
commit 88a361a50e
2 changed files with 126 additions and 14 deletions

View File

@ -577,7 +577,7 @@ AGENT_TOOLS = [
{"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认 apps/modules 下第一个)"}},
{"name":"git_commit_push","description":"git add + commit + pushdevelop 用它提交模块仓库本地提交作为产出证据;无远程只 commit 不 push非 git 目录自动 init","params":{"message":"提交信息","repo_dir":"仓库子目录(可选)"}},
{"name":"ask_question","description":"向用户提问(缺少信息时使用)。需要用户提供文件或结构化信息时用 form 声明动态表单,用户可在待办里直接上传/填写完成","params":{"question":"问题","form":"可选,JSON:{\"fields\":[{\"name\":\"字段名\",\"type\":\"file|text|textarea\",\"label\":\"提示\",\"target\":\"文件目标相对路径(如 env/test.json,目录以/结尾保留原文件名)\",\"required\":true}]}"}},
{"name":"deliver","description":"提交最终交付件(代码/文档文件已用write_file写好时调用","params":{"deliverable_type":"交付件类型(如code_files/design_doc)","summary":"概述","result":"交付件正文","files":"JSON数组[{\"path\":\"modules/{模块}/src/x.py\",\"content\":\"代码内容\"}](可选)"}},
{"name":"deliver","description":"提交最终交付件(代码/文档文件已用write_file写好时调用。deliverable_type 必须用本角色工具schema里声明的合法类型值result 是交付摘要/索引(交付件本体是 write_file 写好的 docs/ 文档与代码文件,系统自动采集文件清单供审核方核对)","params":{"deliverable_type":"交付件类型以工具schema声明的本角色合法值为准","summary":"概述","result":"交付件摘要/索引正文","files":"JSON数组[{\"path\":\"modules/{模块}/src/x.py\",\"content\":\"代码内容\"}](可选)"}},
]
AGENT_SYSTEM_PROMPT = """你是软件开发产线中的「__ROLE__」角色Agent。
@ -617,8 +617,8 @@ __TOOLS__
调工具
{"action":"tool_call","tool":"工具名","params":{}}
提交交付件
{"action":"deliver","deliverable_type":"code_files","summary":"概述","result":"文档内容","files":[{"path":"modules/{模块}/src/file.py","content":"代码"}]}
提交交付件deliverable_type 用工具声明里本角色的合法类型值
{"action":"deliver","deliverable_type":"合法类型值","summary":"概述","result":"交付摘要/索引","files":[{"path":"modules/{模块}/src/file.py","content":"代码"}]}
提问
{"action":"ask","question":"问题"}
@ -1283,12 +1283,38 @@ async def _build_qna_section(sor, task_id, role, agent_id=None):
async def _get_deliverable_content(sor, task_id):
"""取任务最近一次交付件:(content 摘要, deliverable_type, files_json 本体文件清单)。
content 只是摘要/索引本体在 docs/ 编号树超长全文进 content 会撑爆审核上下文
files_json 是引擎自动采集的本体文件相对路径清单审核注入时 PM/QC 按清单 read_file 本体
"""
recs = await sor.sqlExe(
"SELECT content, deliverable_type FROM pipeline_deliverables "
"SELECT content, deliverable_type, files_json FROM pipeline_deliverables "
"WHERE task_id=${tid}$ ORDER BY created_at DESC LIMIT 1", {"tid": task_id})
if recs:
return getattr(recs[0], 'content', '') or '', getattr(recs[0], 'deliverable_type', '') or ''
return '', ''
return (getattr(recs[0], 'content', '') or '',
getattr(recs[0], 'deliverable_type', '') or '',
getattr(recs[0], 'files_json', '') or '')
return '', '', ''
def _build_review_files_block(files_json):
"""交付件本体文件清单区块(追加在 PM/QC 审核输入里)。
2026-09-14 pbls 实测审核注入只有 content[:6000] 摘要QC/PM 可能只看索引打分
人工从库进来也以为 10KB 摘要就是全部产出注入本体清单 + 强制 read_file 指引
"""
try:
files = json.loads(files_json or '[]')
except Exception:
files = []
files = [f for f in files if isinstance(f, str) and f.strip()][:50]
if not files:
return ""
return ("\n\n## 交付件本体文件清单(工作空间相对路径)\n"
"上方 content 只是交付摘要/索引。交付件本体是下列文件——必须逐个 read_file "
"核对真实内容后再逐项判定/评分,禁止只凭摘要放行:\n"
+ "\n".join("- " + f for f in files))
async def _get_project_repos(sor, project_id):
@ -2184,6 +2210,24 @@ _FORCE_PRODUCE_HINT = (
)
def _validate_deliverable_type(role, deliverable_type, allowed_types):
"""deliver 类型守卫:角色声明了合法类型清单时,非法类型拒绝并返回清单(可行动报错)。
2026-09-14 pbls 实测工具描述示例值被 LLM 照抄requirement 交付标成 design_doc
引擎零校验 交付件类型错文件名错-XI6..._design_doc.md审核语义全歪
对齐 create_task 角色守卫模式声明即校验未声明放行逃逸阀不阻塞未适配产线
返回 None=通过返回字符串=拒绝原因回填给 LLM 重试 deliver
"""
if not allowed_types:
return None # 产线未声明 → 不校验
dt = (deliverable_type or '').strip()
if dt in allowed_types:
return None
return (f"FAIL: deliverable_type '{dt or '(空)'}' 不是本角色({role})的合法交付件类型。"
f"合法类型:{''.join(allowed_types)}。请用合法类型重新调用 deliver"
f"(如 {allowed_types[0]})。")
async def _build_fallback_deliverable(space_dir, written_files, role):
"""循环结束未 deliver 时,检测 write_file/git 实际产出,构造真实交付件给 QC而非占位符「Agent未产出交付件」"""
lines = []
@ -2419,6 +2463,25 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
# platform_model_tools分发在 _exec_agent_tool。
from .platform_model_tools import PLATFORM_MODEL_TOOLS_V1
all_tools = AGENT_TOOLS + capability_tools + PLATFORM_MODEL_TOOLS_V1
# ── deliverable_type 守卫2026-09-14 pbls角色声明了合法交付类型时
# ① 工具描述动态改写为只列合法值(源头防照抄示例)② deliver 拦截校验(硬门禁)。
# 未声明的产线两处都放行(逃逸阀,与 create_task 角色守卫同模式)。
allowed_deliverable_types = []
try:
from pipeline_core import get_role_spec
_rspec = get_role_spec(await _resolve_pipeline_id(project_id), role)
allowed_deliverable_types = list(getattr(_rspec, 'deliverable_types', None) or [])
except Exception:
allowed_deliverable_types = []
if allowed_deliverable_types:
_types_txt = '/'.join(allowed_deliverable_types)
all_tools = [
dict(t, params={**t.get('params', {}),
'deliverable_type': f'交付件类型(本角色只能用:{_types_txt}'})
if t.get('name') == 'deliver' else t
for t in all_tools]
tools_text = json.dumps(all_tools, ensure_ascii=False)
# 注入角色技能(角色专属技能全量 + 其余 scope 目录层,优先级 角色>项目>产线>组织>通用)
@ -2505,6 +2568,17 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
params = {}
if tool == "deliver":
# deliverable_type 守卫:非法类型不 break回填 FAIL 让 LLM 换合法类型重交。
# 唯一合法类型时缺省自动填充(纯文本 deliver 常不带类型,不必折腾 LLM
if not (params.get("deliverable_type") or '').strip() \
and len(allowed_deliverable_types) == 1:
params["deliverable_type"] = allowed_deliverable_types[0]
_dt_err = _validate_deliverable_type(
role, params.get("deliverable_type"), allowed_deliverable_types)
if _dt_err:
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _dt_err})
logger.info(f"role_agent deliver type rejected: task={task_id} {_dt_err[:120]}")
continue
deliverable = {"action": "deliver", **params}
break
if tool == "ask_question":
@ -2543,6 +2617,17 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
raw = resp.get("content", "") if isinstance(resp, dict) else str(resp)
act = _parse_agent_action(raw)
if act.get('action') == 'deliver':
# deliverable_type 守卫(文本兜底路径同 native 路径2026-09-14 pbls
if not (act.get("deliverable_type") or '').strip() \
and len(allowed_deliverable_types) == 1:
act["deliverable_type"] = allowed_deliverable_types[0]
_dt_err = _validate_deliverable_type(
role, act.get("deliverable_type"), allowed_deliverable_types)
if _dt_err:
msgs.append({"role": "assistant", "content": raw})
msgs.append({"role": "user", "content": _dt_err})
logger.info(f"role_agent deliver type rejected(text): task={task_id} {_dt_err[:120]}")
continue
deliverable = act
break
elif act.get('action') == 'ask':
@ -2590,7 +2675,9 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
from appPublic.uniqueID import getID
did = getID()
result_text = deliverable.get("result") or ""
deliverable_type = deliverable.get("deliverable_type") or role
# 类型缺省:守卫已保证声明角色只能填合法值;未声明产线空值时用首个合法类型/角色名兜底
deliverable_type = (deliverable.get("deliverable_type") or '').strip() \
or (allowed_deliverable_types[0] if allowed_deliverable_types else role)
summary = deliverable.get("summary", "")
# 写入代码文件
@ -2612,13 +2699,16 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
else:
logger.error(f"code file failed: {abs_path} err={msg}")
# 写交付件文档
# 写交付件文档result 摘要快照,评审留痕用;交付件本体是 docs/ 编号树 + files 清单)
file_path = ''
if result_text:
deliverable_dir = os.path.join(project_dir, 'deliverables', role)
os.makedirs(deliverable_dir, exist_ok=True)
safe_type = (deliverable_type or 'deliverable').replace('/', '_')
file_path = os.path.join(deliverable_dir, f"{task_id}_{safe_type}.md")
safe_type = re.sub(r'[^A-Za-z0-9_.-]', '_', deliverable_type or 'deliverable')
# 文件名清洗2026-09-14 pblsgetID() 随机 ID 可能 '-' 开头
# (实测 -XI6QB04..._design_doc.md'-' 开头文件名在 shell 里被当选项,难用且易误判
safe_tid = re.sub(r'^[^A-Za-z0-9]+', 't', task_id or 'task')
file_path = os.path.join(deliverable_dir, f"{safe_tid}_{safe_type}.md")
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(result_text or '')
@ -2626,10 +2716,23 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
logger.error(f"deliverable write failed: {file_path} err={e}")
file_path = ''
# 交付件本体文件清单工作空间相对路径write_file 实写 + deliver files + 摘要快照。
# PM/QC 审核注入时随清单强制 read_file 本体,不再只看 content 摘要pbls 教训)。
body_files = []
for f in list(written_files) + files_written + ([file_path] if file_path else []):
try:
rel = os.path.relpath(f, space_dir)
except Exception:
continue
if rel and not rel.startswith('..') and rel not in body_files:
body_files.append(rel)
files_json = json.dumps(body_files[:50], ensure_ascii=False)
await sor.C("pipeline_deliverables", {
"id": did, "project_id": project_id, "task_id": task_id,
"deliverable_type": deliverable_type, "title": title,
"content": result_text, "file_path": file_path,
"files_json": files_json,
"quality_score": 80, "review_status": "pending",
"created_by": agent_id or role,
})
@ -3014,7 +3117,7 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
if repos:
await _setup_repos(sor, space_dir, project_id)
deliverable_content, deliverable_type = await _get_deliverable_content(sor, task_id)
deliverable_content, deliverable_type, deliverable_files_json = await _get_deliverable_content(sor, task_id)
if not deliverable_content:
from .task_capability import reject_task
await reject_task(task_id, project_id, who="agent.pm", agent_id=agent_id, comment="没有交付件")
@ -3033,7 +3136,8 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
.replace('__ROLE_SKILLS__', role_skills)
msgs = [{"role": "system", "content": pm_system}]
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}\n\n{content_preview}"})
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}\n\n{content_preview}"
+ _build_review_files_block(deliverable_files_json)})
# 编排缺口通知注入:代码查漏发现的 G1/G2/G3 缺口(此前只进日志没人看),
# 这里随任务上下文交给 PM——PM 可在本回合核实后用 update_task_deps 等修正。
try:
@ -3360,6 +3464,7 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
if task_kind == 'human_task_qc' and human_task_id:
# 人类任务 QC读 pipeline_human_tasks 的处理结果作为检查对象
deliverable_files_json = ''
ht_recs = await sor.sqlExe(
"SELECT title, description, result_data FROM pipeline_human_tasks WHERE id=${hid}$",
{"hid": human_task_id})
@ -3379,7 +3484,7 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
await qc_reject_task(task_id, project_id, who="agent.qc", agent_id=agent_id, comment="人类任务无处理结果")
return {"status": "rejected", "task_id": task_id, "reason": "人类任务无处理结果"}
else:
deliverable_content, deliverable_type = await _get_deliverable_content(sor, task_id)
deliverable_content, deliverable_type, deliverable_files_json = await _get_deliverable_content(sor, task_id)
if not deliverable_content:
from .task_capability import qc_reject_task
await qc_reject_task(task_id, project_id, who="agent.qc", agent_id=agent_id, comment="没有交付件")
@ -3394,7 +3499,8 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
.replace('__ROLE_SKILLS__', role_skills)
msgs = [{"role": "system", "content": qc_system}]
msgs.append({"role": "user", "content": f"请检查以下交付件(类型:{deliverable_type}\n\n{content_preview}"})
msgs.append({"role": "user", "content": f"请检查以下交付件(类型:{deliverable_type}\n\n{content_preview}"
+ _build_review_files_block(deliverable_files_json)})
# 能力工具上下文list_features 需要 project_idQC 审查需求/设计时核对功能落库)
capability_ctx = {

View File

@ -283,6 +283,7 @@ SDL_ROLES = [
system_prompt="""你是需求分析师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.design",
task_title="需求分析",
deliverable_types=["requirement_spec"],
),
RoleSpec(
name="agent.design",
@ -291,6 +292,7 @@ SDL_ROLES = [
system_prompt="""你是系统设计师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.develop",
task_title="应用架构与模块设计",
deliverable_types=["design_doc"],
),
RoleSpec(
name="agent.develop",
@ -299,6 +301,7 @@ SDL_ROLES = [
system_prompt="""你是开发工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.deploy_test",
task_title="应用脚手架开发",
deliverable_types=["code_files"],
),
RoleSpec(
name="agent.deploy_test",
@ -307,6 +310,7 @@ SDL_ROLES = [
system_prompt="""你是测试环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="agent.test",
task_title="部署测试",
deliverable_types=["deploy_doc", "deploy_evidence", "deploy_config"],
),
RoleSpec(
name="agent.test",
@ -316,6 +320,7 @@ SDL_ROLES = [
# 生产部署需人工指令test 通过后不自动派发 deploy_proddeploy_prod 由用户明确指令触发
next_role="",
task_title="功能测试",
deliverable_types=["test_report", "test_docs", "test_plan"],
),
RoleSpec(
name="agent.deploy_prod",
@ -323,6 +328,7 @@ SDL_ROLES = [
aliases=["deploy_production", "production", "release"],
system_prompt="""你是生产环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
next_role="",
deliverable_types=["deploy_doc", "deploy_evidence", "release_notes"],
),
RoleSpec(
name="agent.qc",