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:
parent
7f3ce26295
commit
88a361a50e
@ -577,7 +577,7 @@ AGENT_TOOLS = [
|
||||
{"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认 apps/modules 下第一个)"}},
|
||||
{"name":"git_commit_push","description":"git add + commit + push(develop 用它提交模块仓库本地提交作为产出证据;无远程只 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 pbls):getID() 随机 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_id;QC 审查需求/设计时核对功能落库)
|
||||
capability_ctx = {
|
||||
|
||||
@ -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_prod,deploy_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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user