-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent.py
More file actions
280 lines (244 loc) · 9.9 KB
/
Copy pathagent.py
File metadata and controls
280 lines (244 loc) · 9.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
"""
Agent 会话逻辑
=============
用于运行自动编码会话的核心 Agent 交互函数。
支持多后端:Claude SDK 和 Codex CLI。
"""
import asyncio
from pathlib import Path
from typing import Optional
from base_client import BaseAgentClient, TextEvent, ToolUseEvent, ToolResultEvent
from client_factory import create_client
from progress import print_session_header, print_progress_summary
from prompts import (
get_initializer_prompt,
get_coding_prompt,
get_recovery_prompt,
copy_spec_to_project,
)
from progress import count_feature_statuses
# 配置
AUTO_CONTINUE_DELAY_SECONDS = 3
MAX_INIT_RETRIES = 2 # 初始化失败最大重试次数
STAGNATION_THRESHOLD = 5 # 连续无进展轮数阈值
MAX_RECOVERY_ROUNDS = 2 # 修复回合最大次数
MAX_CONSECUTIVE_ERRORS = 3 # 连续错误最大次数
async def run_agent_session(
client: BaseAgentClient,
message: str,
project_dir: Path,
) -> tuple[str, str]:
"""
运行一次 Agent 会话。
参数:
client: Agent 客户端(支持 Claude 或 Codex)
message: 要发送的提示词
project_dir: 项目目录路径
返回:
(status, response_text),其中 status 为:
- "continue": Agent 应继续工作
- "error": 发生错误
"""
print("正在发送提示词...\n")
try:
# 发送请求
await client.send_message(message)
# 收集响应文本并显示事件
response_text = ""
async for event in client.receive_events():
if isinstance(event, TextEvent):
response_text += event.text
print(event.text, end="", flush=True)
elif isinstance(event, ToolUseEvent):
print(f"\n[Tool: {event.tool_name}]", flush=True)
if event.tool_input:
input_str = event.tool_input
if len(input_str) > 200:
print(f" 输入: {input_str[:200]}...", flush=True)
else:
print(f" 输入: {input_str}", flush=True)
elif isinstance(event, ToolResultEvent):
if event.is_blocked:
print(f" [已阻止] {event.content}", flush=True)
elif event.is_error:
error_str = event.content[:500]
print(f" [错误] {error_str}", flush=True)
else:
print(" [完成]", flush=True)
print("\n" + "-" * 70 + "\n")
if not client.session_succeeded():
return "error", client.get_session_error() or "Agent 会话执行失败"
return "continue", response_text
except Exception as e:
print(f"Agent 会话期间出错: {e}")
return "error", str(e)
async def run_autonomous_agent(
project_dir: Path,
model: str,
backend: str = "claude",
max_iterations: Optional[int] = None,
) -> None:
"""
运行自动 Agent 循环。
参数:
project_dir: 项目目录
model: 要使用的模型
backend: 后端类型 ("claude" 或 "codex")
max_iterations: 最大迭代次数(None 表示不限制)
"""
print("\n" + "=" * 70)
print(" 自动编码 Agent 演示")
print("=" * 70)
print(f"\n项目目录: {project_dir}")
print(f"后端: {backend}")
print(f"模型: {model}")
if max_iterations:
print(f"最大迭代次数: {max_iterations}")
else:
print("最大迭代次数: 不限(将运行至完成)")
print()
# 创建项目目录
project_dir.mkdir(parents=True, exist_ok=True)
# 检查是新开始还是继续
tests_file = project_dir / "feature_list.json"
is_first_run = not tests_file.exists()
if is_first_run:
print("首次启动 - 将使用初始化 Agent")
print()
print("=" * 70)
print(" 注意: 首次会话需要 10-20+ 分钟!")
print(" Agent 正在生成 200 个详细测试用例。")
print(" 这可能看起来像卡住了 - 实际在运行中。请关注 [Tool: ...] 输出。")
print("=" * 70)
print()
# 复制应用规格到项目目录供 Agent 读取
copy_spec_to_project(project_dir)
else:
print("继续现有项目")
print_progress_summary(project_dir)
# 主循环状态
iteration = 0
init_retries = 0
stagnation_count = 0
recovery_rounds = 0
initial_passing, initial_skipped, _ = count_feature_statuses(project_dir)
last_completed = initial_passing + initial_skipped
in_recovery_mode = False
consecutive_errors = 0
while True:
iteration += 1
# === 层1: 确定性完成检测 ===
passing, skipped, total = count_feature_statuses(project_dir)
completed = passing + skipped
if total > 0 and completed == total:
print("\n" + "=" * 70)
if skipped:
print(f" ✅ 所有条目已处理,其中 {skipped} 个为 skipped。")
else:
print(" 🎉 所有测试通过!自动完成。")
print("=" * 70)
break
# 检查最大迭代次数
if max_iterations and iteration > max_iterations:
print(f"\n已达到最大迭代次数 ({max_iterations})")
print("如需继续,请不带 --max-iterations 重新运行脚本")
break
# === 层2: 初始化失败自愈 ===
if is_first_run and iteration > 1:
# 初始化后检查 feature_list.json 是否生成
if not tests_file.exists():
init_retries += 1
if init_retries > MAX_INIT_RETRIES:
print(f"\n初始化失败 {MAX_INIT_RETRIES} 次,停止运行。")
print("请检查 app_spec.txt 或手动创建 feature_list.json")
break
print(
f"\n初始化未生成 feature_list.json,重试 ({init_retries}/{MAX_INIT_RETRIES})..."
)
# 保持 is_first_run = True,继续初始化模式
else:
is_first_run = False # 初始化成功,切换到编码模式
# 打印会话头部
print_session_header(iteration, is_first_run)
try:
# 创建客户端(全新上下文)
client = create_client(backend, project_dir, model)
# 根据会话类型选择提示词
if is_first_run:
prompt = get_initializer_prompt()
elif in_recovery_mode:
prompt = get_recovery_prompt()
print(" [修复模式]")
else:
prompt = get_coding_prompt()
# 使用异步上下文管理器运行会话
async with client:
status, response = await run_agent_session(client, prompt, project_dir)
except Exception as e:
print(f"\n会话启动失败: {e}")
status, response = "error", str(e)
# 处理状态
if status == "continue":
consecutive_errors = 0
print(f"\nAgent 将在 {AUTO_CONTINUE_DELAY_SECONDS} 秒后自动继续...")
print_progress_summary(project_dir)
# === 层3: 停滞检测 ===
current_passing, current_skipped, _ = count_feature_statuses(project_dir)
current_completed = current_passing + current_skipped
if current_completed > last_completed:
# 有进展,重置计数器
stagnation_count = 0
recovery_rounds = 0
in_recovery_mode = False
last_completed = current_completed
else:
# 无进展
stagnation_count += 1
if stagnation_count >= STAGNATION_THRESHOLD:
if not in_recovery_mode:
print(f"\n⚠️ 连续 {stagnation_count} 轮无进展,进入修复模式...")
in_recovery_mode = True
recovery_rounds = 0
else:
recovery_rounds += 1
if recovery_rounds >= MAX_RECOVERY_ROUNDS:
print(
f"\n修复模式已尝试 {MAX_RECOVERY_ROUNDS} 轮仍无进展,停止运行。"
)
print("建议:手动检查失败的测试用例或调整 app_spec.txt")
break
await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)
elif status == "error":
consecutive_errors += 1
print("\n会话遇到错误")
if consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
print(f"连续错误达到 {MAX_CONSECUTIVE_ERRORS} 次,停止运行。")
print("建议:检查后端认证、网络、CLI 可用性或提示词文件。")
break
print(
f"将使用新会话重试... ({consecutive_errors}/{MAX_CONSECUTIVE_ERRORS})"
)
await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)
# 会话之间的短暂延迟
if max_iterations is None or iteration < max_iterations:
print("\n正在准备下一次会话...\n")
await asyncio.sleep(1)
# 最终总结
print("\n" + "=" * 70)
print(" 会话完成")
print("=" * 70)
print(f"\n项目目录: {project_dir}")
print_progress_summary(project_dir)
# 打印运行生成应用的说明
print("\n" + "-" * 70)
print(" 运行生成的应用:")
print("-" * 70)
print(f"\n cd {project_dir.resolve()}")
print(" ./init.sh # 运行初始化脚本")
print(" # 或手动运行:")
print(" npm install && npm run dev")
print(
"\n 然后按终端输出或 init.sh 提示打开对应的本地 URL(Vite 常见为 http://localhost:5173)"
)
print("-" * 70)
print("\n完成!")