ymq 74ce3339d2 v3.4.0: agents produce real code files + git push + PM clones repos
- All agents can write code files via 'files' array in output JSON
- Auto git commit+push after code file creation
- PM agent clones project repos on first review
- PM review shows repository state
- shell_exec now allows ~/pipeline_ws
- Role-specific prompts guide code/file output
2026-08-08 11:42:49 +08:00

633 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Agent loop — 角色agent循环认领执行任务v3.4.0)。
v3.4.0 新增:
- 所有agent具备文件读写 + git pull/push 能力
- agent 产出实际代码文件到项目仓库,而非仅文档
- PM agent 负责 clone 项目关联仓库、初始化工作区
- develop agent 产出可直接运行的源码文件
- 每个agent完成后自动 git commit + push
任务链requirement → design → develop → test → deploy
"""
import asyncio
import json
import os
import subprocess
import logging
logger = logging.getLogger("pipeline.agent_loop")
ROLE_ALIASES = {
'developer': 'develop', 'dev': 'develop', 'development': 'develop', 'coding': 'develop',
'requirement': 'requirement', 'requirements': 'requirement', 'requirement_analysis': 'requirement',
'designer': 'design', 'ui': 'design', 'ux': 'design',
'testing': 'test', 'qa': 'test', 'tester': 'test',
'deployment': 'deploy', 'release': 'deploy',
'operation': 'ops', 'operations': 'ops', 'maintenance': 'ops', '运维': 'ops',
'pm': 'pm', 'project_manager': 'pm', '项目经理': 'pm', 'manager': 'pm',
}
ROLE_CHAIN = {
'requirement': 'design',
'design': 'develop',
'develop': 'test',
'test': 'deploy',
'deploy': None,
}
TASK_REVIEW = 'review'
TASK_APPROVED = 'approved'
# 允许的 shell 工作目录前缀(安全限制)
_ALLOWED_WORKDIRS = [
os.path.expanduser('~/pipeline_ws'),
'/d/pipeline/workspaces',
'/tmp/pipeline_workspaces',
'/tmp/pipeline_ws',
]
def _normalize_role(role):
r = (role or '').strip().lower()
return ROLE_ALIASES.get(r, r)
def _get_db():
from sqlor.dbpools import DBPools
db = DBPools()
if not db.databases:
from appPublic.jsonConfig import getConfig
config = getConfig()
if config.databases:
db.databases = config.databases
return db
def _resolve_workspace(workspace_dir):
if not workspace_dir:
workspace_dir = os.path.expanduser('~/pipeline_ws/default')
try:
parent = os.path.dirname(workspace_dir)
if parent and not os.access(parent, os.W_OK):
proj_name = os.path.basename(workspace_dir)
workspace_dir = os.path.expanduser(f'~/pipeline_ws/{proj_name}')
except Exception:
workspace_dir = os.path.expanduser(f'~/pipeline_ws/{os.path.basename(workspace_dir) or "default"}')
os.makedirs(workspace_dir, exist_ok=True)
return workspace_dir
# ── Agent 工具函数 ──
def _is_safe_workdir(workdir):
"""检查目录是否在允许范围内。"""
wd = os.path.abspath(workdir)
for allowed in _ALLOWED_WORKDIRS:
awd = os.path.abspath(os.path.expanduser(allowed))
if wd.startswith(awd):
return True
return False
async def _run_shell(command, workdir, timeout=120):
"""安全执行 shell 命令。返回 {"rc": int, "stdout": str, "stderr": str}"""
cwd = os.path.abspath(workdir) if workdir else os.path.expanduser('~/pipeline_ws')
if not _is_safe_workdir(cwd):
return {"rc": -1, "stdout": "", "stderr": f"安全限制:目录 {cwd} 不在允许范围"}
if not os.path.isdir(cwd):
return {"rc": -1, "stdout": "", "stderr": f"目录不存在: {cwd}"}
try:
proc = await asyncio.create_subprocess_shell(
command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
cwd=cwd, executable='/bin/bash')
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return {"rc": -1, "stdout": "", "stderr": f"命令超时({timeout}s"}
return {"rc": proc.returncode or 0,
"stdout": stdout.decode('utf-8', 'replace')[-8000:],
"stderr": stderr.decode('utf-8', 'replace')[-4000:]}
except Exception as e:
return {"rc": -1, "stdout": "", "stderr": str(e)[:500]}
async def _write_code_file(filepath, content):
"""写代码文件,自动创建父目录。"""
try:
d = os.path.dirname(filepath)
if d:
os.makedirs(d, exist_ok=True)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content or '')
return True, filepath
except Exception as e:
return False, str(e)
async def _git_setup(workdir):
"""确保 git 用户已配置。"""
r = await _run_shell('git config user.email', workdir, 5)
if 'opencomputing' not in r.get('stdout', '') and '@' not in r.get('stdout', ''):
await _run_shell('git config user.email "pipeline@opencomputing.cn"', workdir, 5)
await _run_shell('git config user.name "Pipeline Agent"', workdir, 5)
async def _git_commit_push(workdir, commit_message, branch='main'):
"""git add → commit → push。"""
await _git_setup(workdir)
r1 = await _run_shell('git add -A', workdir, 10)
r2 = await _run_shell(f'git diff --cached --stat', workdir, 10)
if not r2.get('stdout', '').strip():
return {"rc": 0, "message": "没有变更需要提交"}
r3 = await _run_shell(f'git commit -m "{commit_message}"', workdir, 15)
if r3['rc'] != 0:
return {"rc": r3['rc'], "message": f"commit 失败: {r3['stderr'][:200]}"}
r4 = await _run_shell(f'git push origin {branch}', workdir, 30)
return {"rc": r4['rc'], "message": f"push {'成功' if r4['rc']==0 else '失败'}: {r4['stderr'][:200]}"}
async def _git_clone(repo_url, target_dir, branch='main'):
"""克隆仓库到目标目录。已存在则 pull。"""
if os.path.isdir(os.path.join(target_dir, '.git')):
r = await _run_shell(f'git checkout {branch} && git pull origin {branch}', target_dir, 30)
return {"rc": r['rc'], "message": f"已存在pull: {r['stdout'][:200]}"}
parent = os.path.dirname(target_dir)
os.makedirs(parent, exist_ok=True)
r = await _run_shell(f'git clone -b {branch} {repo_url} {target_dir}', parent, 120)
return {"rc": r['rc'], "message": f"clone: {r['stdout'][:200] if r['rc']==0 else r['stderr'][:200]}"}
# ── Prompts ──
ROLE_PROMPT = """你是软件开发产线项目中的「__ROLE__」角色agent。
当前任务__TITLE__
参数__PARAMS__
__QNA__
## 工作环境
- 项目根目录__WORKSPACE__
- 代码仓库位于__WORKSPACE__/repos/ 下各子目录
- 你有能力读写文件、执行git操作
## 产出要求
__ROLE_SPECIFIC__
## 输出格式严格JSON不要markdown包裹
{
"status": "done",
"deliverable_type": "类型(requirement_doc/design_doc/code_files/test_report/deploy_doc)",
"summary": "一段话概述产出",
"result": "当产出是文档时,这里是完整文档内容",
"files": [
{"path": "repos/仓库名/src/相对路径/文件.java", "content": "文件完整内容"},
{"path": "repos/仓库名/pom.xml", "content": "..."}
],
"git_commit_message": "feat: 简短描述本次变更",
"need_more_info": false,
"question": ""
}
- 如果是文档类产出(requirement/design),用 result 字段输出文档正文
- 如果是代码产出(develop),必须用 files 数组输出每个源码文件,每个元素含 path 和 content
- 测试产出用 result 输出测试报告,如有测试脚本则用 files
- 部署产出用 files 输出部署配置/脚本,用 result 输出部署说明
- 缺少关键信息时:{"status":"need_info","question":"问题"}
## Git 操作说明
- 产出代码文件将自动写入 files 中指定的路径并 git commit + push
- 你只需要在 files 中指定正确的仓库路径即可"""
ROLE_SPECIFICS = {
'requirement': """你是需求分析师。输出完整的需求规格文档Markdown用 result 字段。
文档应包含:项目概述、功能需求、非功能需求、用户角色、业务流程、验收标准。""",
'design': """你是系统设计师。输出完整的设计文档Markdown用 result 字段。
文档应包含:系统架构、技术选型、数据库设计(ER图描述)、API接口设计、前端页面规划、安全设计。
同时可输出 proto/接口定义文件到 files。""",
'develop': """你是开发工程师。你必须输出实际可运行的代码文件!
用 files 数组输出每个源码文件,准确指定在仓库中的路径。
files 中的 path 格式repos/{仓库名}/src/... 或 repos/{仓库名}/pom.xml 等。
每个文件包含完整可运行的代码。
同时用 result 字段输出代码说明。""",
'test': """你是测试工程师。输出测试报告Markdown到 result 字段。
如有测试脚本/用例代码,输出到 files。
报告应包含测试策略、测试用例、执行结果、Bug清单、覆盖率。""",
'deploy': """你是部署运维工程师。输出部署文档Markdown到 result 字段。
必须输出部署配置/脚本到 filesDockerfile/docker-compose.yml/nginx配置等。
files 中的 path 格式repos/{仓库名}/deploy/...""",
}
PM_PROMPT = """你是项目经理PM。审核交付件并推动项目前进。
## 项目
工作目录__WORKSPACE__
关联仓库__REPOS__
## 待审核任务
标题__TITLE__
角色__ROLE__
交付件摘要__SUMMARY__
交付件内容(前6000字)__DELIVERABLE__
## 仓库状态
__REPO_STATE__
## 决策
输出纯JSON
1. 通过:{"status":"approved","comment":"审核意见","next_task_title":"下阶段标题","next_task_description":"详细描述"}
2. 驳回:{"status":"rejected","comment":"驳回原因","questions":"修改要求"}
3. 完成:{"status":"completed","comment":"项目总结"}
审核标准:
- requirement需求是否清晰完整可量化
- design方案合理、覆盖需求、技术可行
- develop代码文件是否实际产出、目录结构是否合理、是否可编译运行
- test测试覆盖充分、发现问题记录完整
- deploy部署配置完整、可一键部署"""
# ── Agent 核心逻辑 ──
async def _claim_task(sor, tenant_id, role, state='submitted', match_role=True):
role = _normalize_role(role)
where_role = "AND (role=${role}$ OR role='')" if match_role else ""
recs = await sor.sqlExe(
"SELECT id, title, params, pipeline_id, tenant_id, role FROM pipeline_tasks "
"WHERE tenant_id=${tid}$ AND state=${state}$ " + where_role + " "
"AND NOT EXISTS (SELECT 1 FROM pipeline_task_steps s WHERE s.task_id=pipeline_tasks.id) "
"ORDER BY created_at ASC LIMIT 1",
{"tid": tenant_id, "state": state, "role": role})
if not recs:
return None
task = recs[0]
task_id = task.id
from appPublic.uniqueID import getID
claim_token = getID()
await sor.sqlExe(
"UPDATE pipeline_tasks SET state='running', claimed_by=${cb}$ "
"WHERE id=${tid}$ AND state=${state}$",
{"cb": claim_token, "tid": task_id, "state": state})
check = await sor.sqlExe(
"SELECT id FROM pipeline_tasks WHERE id=${tid}$ AND state='running' AND claimed_by=${cb}$",
{"tid": task_id, "cb": claim_token})
if not check:
logger.info(f"claim lost race: task={task_id}")
return None
return task
async def _get_workspace_dir(sor, project_id):
recs = await sor.sqlExe("SELECT workspace_dir FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
ws = getattr(recs[0], 'workspace_dir', '') if recs else ''
return _resolve_workspace(ws)
async def _build_qna_section(sor, task_id):
from .questions import get_task_qna
qna = await get_task_qna(task_id)
if not qna:
return ""
lines = ["历史问答:"]
for item in qna:
src = "客户" if item.get('answer_source') == "customer" else "主agent"
lines.append(f"问:{item.get('question','')}")
lines.append(f"答({src}){item.get('answer','')}")
return "\n".join(lines)
def _parse_result(raw):
raw = (raw or "").strip()
if raw.startswith("```"):
raw = raw.split("\n", 1)[1].rsplit("```", 1)[0]
try:
d = json.loads(raw)
if isinstance(d, dict):
return d
except (json.JSONDecodeError, ValueError):
pass
return {"status": "done", "result": raw}
async def _get_deliverable_content(sor, task_id):
recs = await sor.sqlExe(
"SELECT content, deliverable_type 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 '', ''
async def _get_project_repos(sor, project_id):
"""获取项目关联的所有仓库。"""
recs = await sor.sqlExe(
"SELECT id, repo_name, repo_url, default_branch, local_path FROM sd_project_repos "
"WHERE project_id=${pid}$", {"pid": project_id})
return [{'id': getattr(r, 'id', ''), 'name': getattr(r, 'repo_name', ''),
'url': getattr(r, 'repo_url', ''), 'branch': getattr(r, 'default_branch', 'main'),
'local_path': getattr(r, 'local_path', '') or ''} for r in (recs or [])]
async def _setup_repos(sor, workspace_dir, project_id):
"""PMclone 所有项目关联仓库到 workspace/repos/。"""
repos = await _get_project_repos(sor, project_id)
results = []
repos_dir = os.path.join(workspace_dir, 'repos')
os.makedirs(repos_dir, exist_ok=True)
for repo in repos:
target = os.path.join(repos_dir, repo['name'])
r = await _git_clone(repo['url'], target, repo['branch'])
results.append({'repo': repo['name'], **r})
return results
async def _get_repo_state(workspace_dir):
"""获取仓库当前状态供PM审核时查看"""
repos_dir = os.path.join(workspace_dir, 'repos')
if not os.path.isdir(repos_dir):
return "暂无仓库"
lines = []
for name in sorted(os.listdir(repos_dir)):
rp = os.path.join(repos_dir, name)
if os.path.isdir(rp) and os.path.isdir(os.path.join(rp, '.git')):
r = await _run_shell('git log --oneline -3', rp, 5)
lines.append(f"\n[{name}]")
lines.append(r.get('stdout', '') or '(空仓库)')
return "\n".join(lines) if lines else "仓库无提交记录"
async def _get_next_role(current_role):
return ROLE_CHAIN.get(_normalize_role(current_role))
async def _create_next_task(sor, project_id, task, next_role, pm_comment=''):
from appPublic.uniqueID import getID
title = getattr(task, 'title', '') or ''
params_str = getattr(task, 'params', '{}') or '{}'
try:
params = json.loads(params_str) if isinstance(params_str, str) else params_str
except (json.JSONDecodeError, TypeError):
params = {}
new_title = f"{title}{next_role}阶段)"
new_params = {**params, 'previous_role': _normalize_role(getattr(task, 'role', '')),
'previous_task_id': getattr(task, 'id', ''), 'pm_comment': pm_comment}
new_task_id = getID()
await sor.C('pipeline_tasks', {
'id': new_task_id, 'tenant_id': project_id, 'pipeline_id': 'role_task',
'owner_id': 'pm', 'title': new_title,
'params': json.dumps(new_params, ensure_ascii=False),
'role': next_role, 'state': 'submitted', 'claimed_by': None,
})
logger.info(f"_create_next_task: {next_role} task={new_task_id}")
return new_task_id, new_title
# ── 角色 Agent ──
async def role_agent_run(project_id, role, agent_id=None, model_name=None):
role = _normalize_role(role)
if role == 'pm':
return {"status": "idle", "message": "PM请使用 pm_review_run"}
db = _get_db()
async with db.sqlorContext("pipeline") as sor:
task = await _claim_task(sor, project_id, role)
if not task:
return {"status": "idle", "message": "没有待办任务"}
task_id = task.id
title = getattr(task, "title", "") or ""
params_str = getattr(task, "params", "{}") or "{}"
workspace_dir = await _get_workspace_dir(sor, project_id)
# 确保 repos 目录存在
repos_dir = os.path.join(workspace_dir, 'repos')
if not os.path.isdir(repos_dir):
os.makedirs(repos_dir, exist_ok=True)
role_specific = ROLE_SPECIFICS.get(role, ROLE_SPECIFICS.get('develop', ''))
qna_section = await _build_qna_section(sor, task_id)
prompt = (ROLE_PROMPT
.replace('__ROLE__', role)
.replace('__TITLE__', title)
.replace('__PARAMS__', params_str)
.replace('__QNA__', qna_section)
.replace('__WORKSPACE__', workspace_dir)
.replace('__ROLE_SPECIFIC__', role_specific))
from .llm_bridge import llm_call
try:
raw = await llm_call(prompt, model=model_name, temperature=0.4)
except Exception as e:
await sor.sqlExe("UPDATE pipeline_tasks SET state='failed' WHERE id=${tid}$", {"tid": task_id})
logger.error(f"role_agent_run llm failed: task={task_id} err={e}")
return {"status": "failed", "task_id": task_id, "error": str(e)[:200]}
parsed = _parse_result(raw)
if parsed.get("status") == "need_info" and parsed.get("question"):
from .questions import agent_ask
qid = await agent_ask(project_id, task_id, role, parsed["question"],
context={"partial": parsed.get("partial", ""), "title": title})
return {"status": "need_info", "task_id": task_id, "question_id": qid, "question": parsed["question"]}
# ── 处理产出 ──
from appPublic.uniqueID import getID
did = getID()
result_text = parsed.get("result") or raw
deliverable_type = parsed.get("deliverable_type") or role
summary = parsed.get("summary", "")
# 写入实际代码文件(如果有 files 数组)
files_written = []
code_files = parsed.get("files") or []
if isinstance(code_files, list):
for f in code_files:
if isinstance(f, dict) and f.get("path") and f.get("content"):
abs_path = os.path.join(workspace_dir, f["path"])
ok, msg = await _write_code_file(abs_path, f["content"])
if ok:
files_written.append(abs_path)
logger.info(f"code file written: {abs_path}")
else:
logger.error(f"code file failed: {abs_path} err={msg}")
# 写交付件文档
file_path = ''
if result_text:
deliverable_dir = os.path.join(workspace_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")
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(result_text or '')
logger.info(f"deliverable written: {file_path}")
except Exception as e:
logger.error(f"deliverable write failed: {file_path} err={e}")
file_path = ''
# 写DB
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,
"quality_score": 80, "review_status": "pending",
"created_by": agent_id or role,
})
# Git commit + push如果有产出文件
commit_msg = parsed.get("git_commit_message") or f"{role}: {title[:80]}"
git_result = {"rc": 0, "message": "无git操作"}
if files_written:
# 在 repos 目录下的每个 git 仓库各自 commit
for repo_name in os.listdir(repos_dir):
repo_path = os.path.join(repos_dir, repo_name)
if os.path.isdir(os.path.join(repo_path, '.git')):
git_result = await _git_commit_push(repo_path, commit_msg)
# 置为 review 状态
await sor.sqlExe(
"UPDATE pipeline_tasks SET state=${st}$, claimed_by=NULL WHERE id=${tid}$",
{"st": TASK_REVIEW, "tid": task_id})
logger.info(f"role_agent_run completed: task={task_id} role={role} "
f"deliverable={did} files={len(files_written)} git={git_result.get('rc',-1)}")
return {"status": "completed", "task_id": task_id, "deliverable_id": did,
"files_written": len(files_written), "git_result": git_result}
# ── PM 审核 ──
async def pm_review_run(project_id, agent_id=None, model_name=None):
db = _get_db()
async with db.sqlorContext("pipeline") as sor:
task = await _claim_task(sor, project_id, '', state=TASK_REVIEW, match_role=False)
if not task:
return {"status": "idle", "message": "没有待审核任务"}
task_id = task.id
title = getattr(task, "title", "") or ""
task_role = _normalize_role(getattr(task, "role", "") or "")
workspace_dir = await _get_workspace_dir(sor, project_id)
# 首次审核时,确保仓库已 clone
repos = await _get_project_repos(sor, project_id)
if repos:
await _setup_repos(sor, workspace_dir, project_id)
deliverable_content, deliverable_type = await _get_deliverable_content(sor, task_id)
if not deliverable_content:
await sor.sqlExe("UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$", {"tid": task_id})
return {"status": "rejected", "task_id": task_id, "reason": "没有交付件"}
summary = deliverable_content[:200].replace('\n', ' ')
content_preview = deliverable_content[:6000]
if len(deliverable_content) > 6000:
content_preview += "\n...(截断)"
repo_state = await _get_repo_state(workspace_dir)
repos_str = ", ".join([r['name'] for r in repos]) if repos else ""
prompt = (PM_PROMPT
.replace('__TITLE__', title)
.replace('__ROLE__', task_role)
.replace('__SUMMARY__', summary)
.replace('__DELIVERABLE__', content_preview)
.replace('__WORKSPACE__', workspace_dir)
.replace('__REPOS__', repos_str)
.replace('__REPO_STATE__', repo_state))
from .llm_bridge import llm_call
try:
raw = await llm_call(prompt, model=model_name, temperature=0.3)
except Exception as e:
await sor.sqlExe("UPDATE pipeline_tasks SET state='failed' WHERE id=${tid}$", {"tid": task_id})
return {"status": "failed", "task_id": task_id, "error": str(e)[:200]}
parsed = _parse_result(raw)
decision = parsed.get("status", "rejected")
comment = parsed.get("comment", "")
if decision == "approved":
from appPublic.uniqueID import getID
pm_did = getID()
await sor.C("pipeline_deliverables", {
"id": pm_did, "project_id": project_id, "task_id": task_id,
"deliverable_type": "pm_review",
"title": f"PM审核{title}",
"content": json.dumps(parsed, ensure_ascii=False),
"file_path": os.path.join(workspace_dir, 'deliverables', 'pm', f"{task_id}_review.md"),
"quality_score": 100, "review_status": "approved",
"created_by": agent_id or "pm",
})
await sor.sqlExe(
"UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ "
"WHERE task_id=${tid}$", {"cm": comment, "tid": task_id})
await sor.sqlExe(
"UPDATE pipeline_tasks SET state=${st}$ WHERE id=${tid}$",
{"st": TASK_APPROVED, "tid": task_id})
next_role = await _get_next_role(task_role)
if next_role:
next_tid, next_title = await _create_next_task(sor, project_id, task, next_role, comment)
return {"status": "approved", "task_id": task_id,
"next_task_id": next_tid, "next_role": next_role, "comment": comment}
else:
return {"status": "completed", "task_id": task_id, "comment": comment or "项目完成"}
elif decision == "rejected":
from .questions import agent_ask
rejection_q = parsed.get("questions") or comment or "交付件不满足要求"
await agent_ask(project_id, task_id, "pm", rejection_q,
context={"pm_comment": comment, "deliverable_type": deliverable_type})
await sor.sqlExe("UPDATE pipeline_tasks SET state='submitted' WHERE id=${tid}$", {"tid": task_id})
return {"status": "rejected", "task_id": task_id, "comment": comment, "question": rejection_q}
else:
await sor.sqlExe("UPDATE pipeline_tasks SET state='completed' WHERE id=${tid}$", {"tid": task_id})
return {"status": "completed", "task_id": task_id, "comment": comment or "项目完成"}
async def role_agent_loop(project_id, role, agent_id=None, model_name=None, max_iterations=10):
results = []
for _ in range(max_iterations):
r = await role_agent_run(project_id, role, agent_id, model_name)
results.append(r)
if r["status"] in ("idle", "need_info", "failed"):
break
await asyncio.sleep(1)
return results
async def pm_review_loop(project_id, agent_id=None, model_name=None, max_iterations=20):
results = []
for _ in range(max_iterations):
r = await pm_review_run(project_id, agent_id, model_name)
results.append(r)
if r["status"] in ("idle", "failed"):
break
await asyncio.sleep(1)
return results
# 向后兼容
async def run_agent_loop(project_id, role_name, model_name=None):
role = _normalize_role(role_name or "develop")
if role == 'pm':
return await pm_review_run(project_id, model_name=model_name)
return await role_agent_run(project_id, role_name or "develop", model_name=model_name)
async def agent_loop(project_id, role_name, model_name=None, max_iterations=10):
role = _normalize_role(role_name or "develop")
if role == 'pm':
return await pm_review_loop(project_id, model_name=model_name, max_iterations=max_iterations)
return await role_agent_loop(project_id, role_name or "develop", model_name=model_name, max_iterations=max_iterations)